record: add native FLV and RTMP publish protocol

Add bounded FLV muxing and worker-owned RTMP framing, handshake, control and AMF publish negotiation. Retain borrowed output across short writes and cancellation without frame-path I/O or waits.

Exercise standard and legacy publish sessions against an independent receiver, including name-only onFCPublish callbacks. Socket transport and native encoder delivery remain separate integration work.
This commit is contained in:
Codex committed 2026-10-29 20:19:51 +01:00
1 parent 60d9cd7bdd
commit b2356920ec
38 files changed
+4744

No files matched your search

@@ -139,6 +139,26 @@ jobs:
make clean && make -j$(nproc) check CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
make clean
- name: Native FLV mux (ASan + UBSan, mingw-w64)
shell: bash
working-directory: samples/record/flv_mux
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
make clean && make check stream-check SANITIZER=address,undefined
make clean && make check stream-check CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
make clean
- name: RTMP output chunks (ASan + UBSan, mingw-w64)
shell: bash
working-directory: samples/record/rtmp_chunk
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
make clean && make check peer-check SANITIZER=address,undefined
make clean && make check peer-check CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
make clean
- name: GPU PCM output pipeline (ASan + UBSan, TSan, mingw-w64)
working-directory: samples/record/gpu_av_output
run: |
+303
View File
@@ -0,0 +1,303 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "record_mux_flv.h"
#include <stdlib.h>
#include <string.h>
enum flv_output
{
FLV_HEADER, FLV_VIDEO_CONFIG, FLV_AUDIO_CONFIG, FLV_MEDIA, FLV_IDLE
};
struct record_mux_flv
{
uint64_t last[2], input_dts;
record_flv_span_t spans[3];
uint8_t *config;
const uint8_t *input;
size_t avcc_size, asc_size, limit, size, offset, span_count;
uint32_t timestamp;
int32_t composition;
unsigned timescale[2], track;
enum flv_output output;
uint8_t prefix[16], suffix[4];
bool pending, scanning, failed, finished, seen[2], keyframe, vcl, idr;
};
static void flv_uint(uint8_t *out, uint32_t value, unsigned count)
{
unsigned i;
for (i = 0; i < count; i++)
out[count - i - 1] = (uint8_t)(value >> (i * 8));
}
static bool flv_sets(const uint8_t *data, size_t size, size_t *pos,
unsigned count, unsigned type)
{
unsigned i, length;
for (i = 0; i < count; i++)
{
if (size - *pos < 2)
return false;
length = ((unsigned)data[*pos] << 8) | data[*pos + 1];
*pos += 2;
if (!length || length > size - *pos || (data[*pos] & 0x80)
|| (data[*pos] & 31) != type)
return false;
*pos += length;
}
return true;
}
static bool flv_avcc(const uint8_t *data, size_t size)
{
size_t pos = 6;
unsigned count;
if (!data || size < 7 || size > 65536 || data[0] != 1
|| data[4] != 0xff || (data[5] & 0xe0) != 0xe0 || !(data[5] & 31))
return false;
if (!flv_sets(data, size, &pos, data[5] & 31, 7) || pos == size)
return false;
count = data[pos++];
if (!count || !flv_sets(data, size, &pos, count, 8))
return false;
if (pos == size)
return true;
if (size - pos < 4 || (data[pos] & 0xfc) != 0xfc
|| (data[pos + 1] & 0xf8) != 0xf8 || (data[pos + 2] & 0xf8) != 0xf8)
return false;
pos += 3;
count = data[pos++];
return flv_sets(data, size, &pos, count, 13) && pos == size;
}
static unsigned flv_bits(const uint8_t *data, size_t size, unsigned *pos, unsigned count)
{
unsigned i, value = 0;
if (*pos > size * 8 || count > size * 8 - *pos)
return ~0u;
for (i = 0; i < count; i++, (*pos)++)
value = (value << 1) | ((data[*pos / 8] >> (7 - *pos % 8)) & 1);
return value;
}
static bool flv_asc(const uint8_t *data, size_t size)
{
unsigned pos = 0, index, channels;
if (!data || size < 2 || size > 64 || flv_bits(data, size, &pos, 5) != 2)
return false;
index = flv_bits(data, size, &pos, 4);
if (index == 13 || index == 14)
return false;
if (index == 15)
{
unsigned rate = flv_bits(data, size, &pos, 24);
if (!rate || rate == ~0u)
return false;
}
channels = flv_bits(data, size, &pos, 4);
if (!channels || channels > 7 || flv_bits(data, size, &pos, 1) == ~0u)
return false;
return flv_bits(data, size, &pos, 2) == 0;
}
static void flv_tag(record_mux_flv_t *m, unsigned track, const void *data,
size_t size, uint32_t timestamp, int32_t composition, bool keyframe, bool config)
{
unsigned bytes = track == 1 ? 5 : 2;
memset(m->prefix, 0, sizeof(m->prefix));
m->prefix[0] = track == 1 ? 9 : 8;
flv_uint(m->prefix + 1, (uint32_t)size + bytes, 3);
flv_uint(m->prefix + 4, timestamp, 3);
m->prefix[7] = (uint8_t)(timestamp >> 24);
m->prefix[11] = track == 1 ? (keyframe ? 0x17 : 0x27) : 0xaf;
m->prefix[12] = config ? 0 : 1;
if (track == 1)
flv_uint(m->prefix + 13, (uint32_t)composition, 3);
flv_uint(m->suffix, (uint32_t)size + bytes + 11, 4);
m->spans[0].data = m->prefix;
m->spans[0].size = 11 + bytes;
m->spans[1].data = data;
m->spans[1].size = size;
m->spans[2].data = m->suffix;
m->spans[2].size = 4;
m->span_count = 3;
m->pending = true;
}
record_mux_flv_t *record_mux_flv_new(const record_flv_config_t *c)
{
record_mux_flv_t *m;
if (!c || (!c->avcc && !c->asc) || !c->max_packet_size || c->max_packet_size > 0xfffffa
|| (c->avcc ? !c->video_timescale || !flv_avcc((const uint8_t*)c->avcc, c->avcc_size)
: c->avcc_size || c->video_timescale)
|| (c->asc ? !c->audio_timescale || !flv_asc((const uint8_t*)c->asc, c->asc_size)
: c->asc_size || c->audio_timescale))
return NULL;
m = (record_mux_flv_t*)calloc(1, sizeof(*m));
if (!m)
return NULL;
m->config = (uint8_t*)malloc(c->avcc_size + c->asc_size);
if (!m->config)
{
free(m);
return NULL;
}
if (c->avcc_size)
memcpy(m->config, c->avcc, c->avcc_size);
if (c->asc_size)
memcpy(m->config + c->avcc_size, c->asc, c->asc_size);
m->avcc_size = c->avcc_size;
m->asc_size = c->asc_size;
m->timescale[0] = c->video_timescale;
m->timescale[1] = c->audio_timescale;
m->limit = c->max_packet_size;
memcpy(m->prefix, "FLV\1\0\0\0\0\11\0\0\0\0", 13);
m->prefix[4] = (c->avcc ? 1 : 0) | (c->asc ? 4 : 0);
m->spans[0].data = m->prefix;
m->spans[0].size = 13;
m->span_count = 1;
m->pending = true;
return m;
}
static bool flv_milliseconds(uint64_t ticks, unsigned scale, uint32_t *result)
{
uint64_t seconds = ticks / scale;
uint64_t ms;
if (seconds > 0x7fffffff / 1000)
return false;
ms = seconds * 1000 + (ticks % scale) * 1000 / scale;
if (ms > 0x7fffffff)
return false;
*result = (uint32_t)ms;
return true;
}
int record_mux_flv_push(record_mux_flv_t *m, unsigned track, const void *data,
size_t size, uint64_t dts, uint64_t pts, bool keyframe)
{
uint32_t dts_ms, pts_ms;
int64_t composition;
if (!m || m->failed || m->finished)
return RECORD_FLV_ERROR;
if (m->pending || m->scanning)
return RECORD_FLV_BUSY;
if (track < 1 || track > 2 || !m->timescale[track - 1] || !data || !size || size > m->limit
|| (m->seen[track - 1] && dts < m->last[track - 1])
|| !flv_milliseconds(dts, m->timescale[track - 1], &dts_ms)
|| !flv_milliseconds(pts, m->timescale[track - 1], &pts_ms))
return RECORD_FLV_ERROR;
composition = (int64_t)pts_ms - dts_ms;
if ((track == 1 && (composition < -0x800000 || composition > 0x7fffff
|| (!m->seen[0] && !keyframe))) || (track == 2 && pts != dts))
return RECORD_FLV_ERROR;
m->track = track;
m->input = (const uint8_t*)data;
m->size = size;
m->offset = 0;
m->input_dts = dts;
m->timestamp = dts_ms;
m->composition = (int32_t)composition;
m->keyframe = keyframe;
m->vcl = m->idr = false;
m->output = FLV_MEDIA;
m->scanning = track == 1;
if (track == 2)
flv_tag(m, track, data, size, dts_ms, 0, false, false);
return RECORD_FLV_PROGRESS;
}
int record_mux_flv_step(record_mux_flv_t *m)
{
uint32_t length;
unsigned type;
if (!m || m->failed)
return RECORD_FLV_ERROR;
if (m->pending)
return RECORD_FLV_PACKET;
if (m->finished)
return RECORD_FLV_END;
if (!m->scanning)
return RECORD_FLV_BUSY;
if (m->offset == m->size)
{
if (!m->vcl || (!m->seen[0] && !m->idr))
goto failed;
m->scanning = false;
flv_tag(m, 1, m->input, m->size, m->timestamp, m->composition, m->keyframe, false);
return RECORD_FLV_PACKET;
}
if (m->size - m->offset < 4)
goto failed;
length = ((uint32_t)m->input[m->offset] << 24) | ((uint32_t)m->input[m->offset + 1] << 16)
| ((uint32_t)m->input[m->offset + 2] << 8) | m->input[m->offset + 3];
m->offset += 4;
if (!length || length > m->size - m->offset || (m->input[m->offset] & 0x80))
goto failed;
type = m->input[m->offset] & 31;
if (!type || type >= 24)
goto failed;
m->vcl = m->vcl || type <= 5;
m->idr = m->idr || type == 5;
m->offset += length;
return RECORD_FLV_PROGRESS;
failed:
m->failed = true;
m->scanning = false;
m->input = NULL;
return RECORD_FLV_ERROR;
}
bool record_mux_flv_get(record_mux_flv_t *m, const record_flv_span_t **spans, size_t *count)
{
if (!m || !spans || !count || !m->pending)
return false;
*spans = m->spans;
*count = m->span_count;
return true;
}
bool record_mux_flv_release(record_mux_flv_t *m)
{
if (!m || !m->pending)
return false;
m->pending = false;
if (m->output == FLV_HEADER && m->avcc_size)
{
m->output = FLV_VIDEO_CONFIG;
flv_tag(m, 1, m->config, m->avcc_size, 0, 0, true, true);
}
else if ((m->output == FLV_HEADER || m->output == FLV_VIDEO_CONFIG) && m->asc_size)
{
m->output = FLV_AUDIO_CONFIG;
flv_tag(m, 2, m->config + m->avcc_size, m->asc_size, 0, 0, false, true);
}
else
{
if (m->output == FLV_MEDIA)
{
m->last[m->track - 1] = m->input_dts;
m->seen[m->track - 1] = true;
}
m->output = FLV_IDLE;
m->input = NULL;
m->span_count = 0;
}
return true;
}
bool record_mux_flv_finish(record_mux_flv_t *m)
{
if (!m || m->failed || m->pending || m->scanning)
return false;
m->finished = true;
return true;
}
void record_mux_flv_free(record_mux_flv_t *m)
{
if (!m)
return;
free(m->config);
free(m);
}
+49
View File
@@ -0,0 +1,49 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef RECORD_MUX_FLV_H
#define RECORD_MUX_FLV_H
#include <stddef.h>
#include <stdint.h>
#include <boolean.h>
#include <retro_common_api.h>
typedef struct record_mux_flv record_mux_flv_t;
typedef struct record_flv_config
{
const void *avcc, *asc;
size_t avcc_size, asc_size, max_packet_size;
unsigned video_timescale, audio_timescale;
} record_flv_config_t;
typedef struct record_flv_span
{
const void *data;
size_t size;
} record_flv_span_t;
enum record_flv_result
{
RECORD_FLV_ERROR = -1, RECORD_FLV_BUSY, RECORD_FLV_PROGRESS,
RECORD_FLV_PACKET, RECORD_FLV_END
};
RETRO_BEGIN_DECLS
/* Single worker owner. Copies AVCDecoderConfigurationRecord and AAC-LC ASC
* during cold initialization. At least one track; AVC uses four-byte lengths. */
record_mux_flv_t *record_mux_flv_new(const record_flv_config_t *config);
/* Track 1: one length-prefixed AVC access unit; track 2: one raw AAC unit.
* Accepted input stays borrowed until output release, error or free.
* BUSY takes nothing. Timestamps use the configured per-track timescale;
* their millisecond representation must fit a nonnegative signed 32-bit value. */
int record_mux_flv_push(record_mux_flv_t *mux, unsigned track,
const void *data, size_t size, uint64_t dts, uint64_t pts, bool keyframe);
/* One AVC NAL validation step. PACKET repeats until release; BUSY needs input. */
int record_mux_flv_step(record_mux_flv_t *mux);
/* Up to three borrowed spans. No payload copy, allocation, I/O or wait. */
bool record_mux_flv_get(record_mux_flv_t *mux,
const record_flv_span_t **spans, size_t *count);
bool record_mux_flv_release(record_mux_flv_t *mux);
/* Requires acknowledged output. No seek or file trailer. */
bool record_mux_flv_finish(record_mux_flv_t *mux);
void record_mux_flv_free(record_mux_flv_t *mux);
RETRO_END_DECLS
#endif
+159
View File
@@ -0,0 +1,159 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "record_rtmp_chunk.h"
#include <stdlib.h>
#include <string.h>
struct record_rtmp_chunk
{
record_rtmp_span_t spans[3];
size_t count, index, offset, remaining, in_chunk, header_size, header_offset;
uint32_t timestamp;
unsigned chunk_size, chunk_id;
uint8_t header[18];
bool active;
};
static void rtmp_be(uint8_t *p, uint32_t v, unsigned n)
{
unsigned i;
for (i = 0; i < n; i++)
p[n - i - 1] = (uint8_t)(v >> (i * 8));
}
static unsigned rtmp_basic(uint8_t *p, unsigned fmt, unsigned id)
{
if (id < 64)
{
p[0] = (uint8_t)((fmt << 6) | id);
return 1;
}
id -= 64;
p[0] = (uint8_t)((fmt << 6) | (id >= 256 ? 1 : 0));
p[1] = (uint8_t)id;
if (id < 256)
return 2;
p[2] = (uint8_t)(id >> 8);
return 3;
}
static void rtmp_header(record_rtmp_chunk_t *c, bool first,
uint32_t stream, unsigned type)
{
unsigned n = rtmp_basic(c->header, first ? 0 : 3, c->chunk_id);
if (first)
{
rtmp_be(c->header + n, c->timestamp < 0xffffff ? c->timestamp : 0xffffff, 3);
rtmp_be(c->header + n + 3, (uint32_t)c->remaining, 3);
c->header[n + 6] = (uint8_t)type;
c->header[n + 7] = (uint8_t)stream;
c->header[n + 8] = (uint8_t)(stream >> 8);
c->header[n + 9] = (uint8_t)(stream >> 16);
c->header[n + 10] = (uint8_t)(stream >> 24);
n += 11;
}
if (c->timestamp >= 0xffffff)
{
rtmp_be(c->header + n, c->timestamp, 4);
n += 4;
}
c->header_size = n;
c->header_offset = 0;
c->in_chunk = c->remaining < c->chunk_size ? c->remaining : c->chunk_size;
}
record_rtmp_chunk_t *record_rtmp_chunk_new(unsigned size)
{
record_rtmp_chunk_t *c;
if (!size || size > 0x7fffffff)
return NULL;
c = (record_rtmp_chunk_t*)calloc(1, sizeof(*c));
if (c)
c->chunk_size = size;
return c;
}
bool record_rtmp_chunk_size(record_rtmp_chunk_t *c, unsigned size)
{
if (!c || c->active || !size || size > 0x7fffffff)
return false;
c->chunk_size = size;
return true;
}
bool record_rtmp_chunk_push(record_rtmp_chunk_t *c, unsigned id,
uint32_t stream, unsigned type, uint32_t timestamp,
const record_rtmp_span_t *spans, size_t count)
{
size_t i, total = 0;
if (!c || c->active || id < 2 || id > 65599 || !type || type > 255
|| count > 3 || (count && !spans))
return false;
for (i = 0; i < count; i++)
{
if (!spans[i].data || !spans[i].size || spans[i].size > 0xffffff - total)
return false;
total += spans[i].size;
}
for (i = 0; i < count; i++)
c->spans[i] = spans[i];
c->count = count;
c->index = c->offset = 0;
c->remaining = total;
c->timestamp = timestamp;
c->chunk_id = id;
c->active = true;
rtmp_header(c, true, stream, type);
return true;
}
bool record_rtmp_chunk_get(record_rtmp_chunk_t *c, record_rtmp_span_t *span)
{
size_t left;
if (!c || !span || !c->active)
return false;
if (c->header_offset < c->header_size)
{
span->data = c->header + c->header_offset;
span->size = c->header_size - c->header_offset;
return true;
}
left = c->spans[c->index].size - c->offset;
span->data = (const uint8_t*)c->spans[c->index].data + c->offset;
span->size = left < c->in_chunk ? left : c->in_chunk;
return true;
}
bool record_rtmp_chunk_consume(record_rtmp_chunk_t *c, size_t size)
{
record_rtmp_span_t span;
if (!record_rtmp_chunk_get(c, &span) || size > span.size)
return false;
if (!size)
return true;
if (c->header_offset < c->header_size)
c->header_offset += size;
else
{
c->offset += size;
c->remaining -= size;
c->in_chunk -= size;
if (c->offset == c->spans[c->index].size)
{
c->index++;
c->offset = 0;
}
if (!c->in_chunk && c->remaining)
rtmp_header(c, false, 0, 0);
}
if (!c->remaining && c->header_offset == c->header_size)
{
c->active = false;
memset(c->spans, 0, sizeof(c->spans));
}
return true;
}
void record_rtmp_chunk_free(record_rtmp_chunk_t *c)
{
free(c);
}
+32
View File
@@ -0,0 +1,32 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef RECORD_RTMP_CHUNK_H
#define RECORD_RTMP_CHUNK_H
#include <stddef.h>
#include <stdint.h>
#include <boolean.h>
#include <retro_common_api.h>
typedef struct record_rtmp_chunk record_rtmp_chunk_t;
typedef struct record_rtmp_span
{
const void *data;
size_t size;
} record_rtmp_span_t;
RETRO_BEGIN_DECLS
/* Single worker owner. The peer must be told before changing chunk size. */
record_rtmp_chunk_t *record_rtmp_chunk_new(unsigned chunk_size);
bool record_rtmp_chunk_size(record_rtmp_chunk_t *chunk, unsigned chunk_size);
/* Up to three spans, borrowed until all output is consumed or free.
* False takes nothing. Stream ID is the RTMP message stream, not chunk ID. */
bool record_rtmp_chunk_push(record_rtmp_chunk_t *chunk, unsigned chunk_id,
uint32_t stream_id, unsigned type, uint32_t timestamp,
const record_rtmp_span_t *spans, size_t count);
/* One header or borrowed payload span, limited to the current chunk.
* Partial consumption supports short writes; zero consumption is a no-op.
* No allocation, copy of payload, I/O, loop over chunks or wait. */
bool record_rtmp_chunk_get(record_rtmp_chunk_t *chunk, record_rtmp_span_t *span);
bool record_rtmp_chunk_consume(record_rtmp_chunk_t *chunk, size_t size);
void record_rtmp_chunk_free(record_rtmp_chunk_t *chunk);
RETRO_END_DECLS
#endif
+134
View File
@@ -0,0 +1,134 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "record_rtmp_command.h"
#include <string.h>
struct command_writer
{
uint8_t *data;
size_t capacity, used;
bool failed;
};
static void command_bytes(struct command_writer *w, const void *data, size_t size)
{
if (w->failed || size > w->capacity - w->used)
{
w->failed = true;
return;
}
memcpy(w->data + w->used, data, size);
w->used += size;
}
static void command_marker(struct command_writer *w, uint8_t marker)
{
command_bytes(w, &marker, 1);
}
static void command_string(struct command_writer *w, const char *text, bool property)
{
size_t size = 0;
uint8_t length[2];
if (!text)
{
w->failed = true;
return;
}
while (size <= 4096 && text[size])
size++;
if (size > 4096)
{
w->failed = true;
return;
}
if (!property)
command_marker(w, 2);
length[0] = (uint8_t)(size >> 8);
length[1] = (uint8_t)size;
command_bytes(w, length, 2);
command_bytes(w, text, size);
}
static void command_number(struct command_writer *w, uint32_t value)
{
uint64_t bits = 0;
uint32_t high = value;
unsigned bit = 0, i;
uint8_t encoded[9];
if (value)
{
while (high >>= 1)
bit++;
bits = ((uint64_t)(1023 + bit) << 52)
| (((uint64_t)value << (52 - bit)) & (((uint64_t)1 << 52) - 1));
}
encoded[0] = 0;
for (i = 0; i < 8; i++)
encoded[i + 1] = (uint8_t)(bits >> (56 - i * 8));
command_bytes(w, encoded, sizeof(encoded));
}
static void command_property(struct command_writer *w, const char *name, const char *value)
{
command_string(w, name, true);
command_string(w, value, false);
}
bool record_rtmp_command_encode(enum record_rtmp_command command,
const record_rtmp_command_config_t *c, void *output, size_t capacity, size_t *size)
{
static const char *const names[] =
{ "connect", "createStream", "publish", "deleteStream", "releaseStream", "FCPublish" };
struct command_writer w;
uint32_t transaction;
if (size)
*size = 0;
if (!size || !c || !output || !capacity || command < RECORD_RTMP_CONNECT
|| command > RECORD_RTMP_FC_PUBLISH)
return false;
transaction = command == RECORD_RTMP_CONNECT ? 1
: command == RECORD_RTMP_PUBLISH || command == RECORD_RTMP_DELETE_STREAM ? 0 : c->transaction;
if ((command == RECORD_RTMP_CREATE_STREAM || command == RECORD_RTMP_RELEASE_STREAM
|| command == RECORD_RTMP_FC_PUBLISH) && !transaction)
return false;
if (command == RECORD_RTMP_DELETE_STREAM && !c->stream)
return false;
if ((command == RECORD_RTMP_PUBLISH || command == RECORD_RTMP_RELEASE_STREAM
|| command == RECORD_RTMP_FC_PUBLISH) && (!c->name || !*c->name))
return false;
if (command == RECORD_RTMP_CONNECT && (!c->application || !*c->application || !c->url || !*c->url))
return false;
w.data = (uint8_t*)output;
w.capacity = capacity;
w.used = 0;
w.failed = false;
command_string(&w, names[command], false);
command_number(&w, transaction);
if (command == RECORD_RTMP_CONNECT)
{
command_marker(&w, 3);
command_property(&w, "app", c->application);
command_property(&w, "tcUrl", c->url);
command_property(&w, "flashVer", "FMLE/3.0 (compatible; RetroArch)");
command_string(&w, "fpad", true);
command_bytes(&w, "\1\0", 2);
command_string(&w, "objectEncoding", true);
command_number(&w, 0);
command_bytes(&w, "\0\0\11", 3);
}
else
{
command_marker(&w, 5);
if (command == RECORD_RTMP_PUBLISH || command == RECORD_RTMP_RELEASE_STREAM
|| command == RECORD_RTMP_FC_PUBLISH)
command_string(&w, c->name, false);
if (command == RECORD_RTMP_PUBLISH)
command_string(&w, "live", false);
if (command == RECORD_RTMP_DELETE_STREAM)
command_number(&w, c->stream);
}
if (w.failed)
return false;
*size = w.used;
return true;
}
+30
View File
@@ -0,0 +1,30 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef RECORD_RTMP_COMMAND_H
#define RECORD_RTMP_COMMAND_H
#include "record_rtmp_chunk.h"
enum record_rtmp_command
{
RECORD_RTMP_CONNECT,
RECORD_RTMP_CREATE_STREAM,
RECORD_RTMP_PUBLISH,
RECORD_RTMP_DELETE_STREAM,
RECORD_RTMP_RELEASE_STREAM,
RECORD_RTMP_FC_PUBLISH
};
typedef struct record_rtmp_command_config
{
const char *application, *url, *name;
uint32_t transaction, stream;
} record_rtmp_command_config_t;
RETRO_BEGIN_DECLS
/* Encodes one AMF0 command body into caller storage; no I/O or allocation.
* Strings are UTF-8, at most 4096 bytes each, and must not alias output.
* Failure leaves size zero; partial output must not be transmitted.
* Connect uses transaction 1; publish/delete use 0; other commands use config.
* Publish targets the assigned message stream; other commands target stream 0. */
bool record_rtmp_command_encode(enum record_rtmp_command command,
const record_rtmp_command_config_t *config,
void *output, size_t capacity, size_t *size);
RETRO_END_DECLS
#endif
+201
View File
@@ -0,0 +1,201 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "record_rtmp_control.h"
#include <stdlib.h>
struct record_rtmp_control
{
uint64_t received, acknowledged, output_ack;
uint32_t window, bandwidth, peer_ack, ping, response_window;
unsigned limit_type, output_type;
uint8_t output[6];
bool failed, ping_pending, window_pending, borrowed;
};
static uint32_t control_read(const uint8_t *p)
{
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16)
| ((uint32_t)p[2] << 8) | p[3];
}
static void control_write(uint8_t *p, uint32_t value)
{
p[0] = (uint8_t)(value >> 24);
p[1] = (uint8_t)(value >> 16);
p[2] = (uint8_t)(value >> 8);
p[3] = (uint8_t)value;
}
record_rtmp_control_t *record_rtmp_control_new(void)
{
record_rtmp_control_t *c = (record_rtmp_control_t*)calloc(1, sizeof(*c));
if (c)
c->limit_type = 2;
return c;
}
bool record_rtmp_control_received(record_rtmp_control_t *c, size_t size)
{
if (!c || c->failed)
return false;
if ((uint64_t)size > (uint64_t)-1 - c->received)
{
c->failed = true;
return false;
}
c->received += size;
return true;
}
int record_rtmp_control_handle(record_rtmp_control_t *c,
record_rtmp_receive_t *receive, const record_rtmp_message_t *m)
{
const uint8_t *p;
uint32_t value, limit;
unsigned event, kind;
if (!c || c->failed || !m)
return RECORD_RTMP_CONTROL_ERROR;
if (m->type < 1 || m->type > 6)
return RECORD_RTMP_CONTROL_OTHER;
p = (const uint8_t*)m->payload.data;
if (!p || m->stream_id || (m->type != 4 && m->chunk_id != 2))
goto invalid;
if (m->type == 4)
{
if (m->payload.size < 2)
goto invalid;
event = ((unsigned)p[0] << 8) | p[1];
if (event == 3)
{
if (m->payload.size != 10)
goto invalid;
}
else if (event <= 4 || event == 6 || event == 7)
{
if (m->payload.size != 6)
goto invalid;
if (event == 6)
{
if (c->ping_pending)
return RECORD_RTMP_CONTROL_BUSY;
c->ping = control_read(p + 2);
c->ping_pending = true;
}
}
return RECORD_RTMP_CONTROL_HANDLED;
}
if (m->payload.size != (m->type == 6 ? 5 : 4))
goto invalid;
value = control_read(p);
switch (m->type)
{
case 1:
if (!record_rtmp_receive_size(receive, value))
goto invalid;
break;
case 2:
if (!record_rtmp_receive_abort(receive, value))
goto invalid;
break;
case 3:
c->peer_ack = value;
break;
case 5:
if (!value)
goto invalid;
c->window = value;
break;
case 6:
kind = p[4];
if (!value || kind > 2)
goto invalid;
if (kind == 2 && c->limit_type != 0)
break;
limit = kind == 1 && c->bandwidth && c->bandwidth < value
? c->bandwidth : value;
if (limit != c->bandwidth && c->window_pending)
return RECORD_RTMP_CONTROL_BUSY;
if (limit != c->bandwidth)
{
c->response_window = limit;
c->window_pending = true;
}
c->bandwidth = limit;
c->limit_type = kind == 2 ? 0 : kind;
break;
}
return RECORD_RTMP_CONTROL_HANDLED;
invalid:
c->failed = true;
return RECORD_RTMP_CONTROL_ERROR;
}
bool record_rtmp_control_get(record_rtmp_control_t *c, record_rtmp_message_t *m)
{
if (!c || !m || c->failed)
return false;
if (!c->borrowed)
{
if (c->ping_pending)
{
c->output_type = 4;
c->output[0] = 0;
c->output[1] = 7;
control_write(c->output + 2, c->ping);
}
else if (c->window_pending)
{
c->output_type = 5;
control_write(c->output, c->response_window);
}
else if (c->window && c->received - c->acknowledged >= c->window)
{
c->output_type = 3;
c->output_ack = c->received;
control_write(c->output, (uint32_t)c->received);
}
else
return false;
c->borrowed = true;
}
m->payload.data = c->output;
m->payload.size = c->output_type == 4 ? 6 : 4;
m->type = c->output_type;
m->chunk_id = 2;
m->stream_id = 0;
m->timestamp = 0;
return true;
}
bool record_rtmp_control_ack(record_rtmp_control_t *c)
{
if (!c || !c->borrowed)
return false;
if (c->output_type == 4)
c->ping_pending = false;
else if (c->output_type == 5)
c->window_pending = false;
else
c->acknowledged = c->output_ack;
c->borrowed = false;
return true;
}
bool record_rtmp_control_status(const record_rtmp_control_t *c,
record_rtmp_control_status_t *s)
{
if (!c || !s || c->failed)
return false;
s->received = c->received;
s->peer_ack = c->peer_ack;
s->bandwidth = c->bandwidth;
s->window = c->window;
return true;
}
bool record_rtmp_control_free(record_rtmp_control_t *c)
{
if (c && c->borrowed)
return false;
free(c);
return true;
}
+37
View File
@@ -0,0 +1,37 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef RECORD_RTMP_CONTROL_H
#define RECORD_RTMP_CONTROL_H
#include "record_rtmp_receive.h"
typedef struct record_rtmp_control record_rtmp_control_t;
typedef struct record_rtmp_control_status
{
uint64_t received;
uint32_t peer_ack, bandwidth, window;
} record_rtmp_control_status_t;
enum record_rtmp_control_result
{
RECORD_RTMP_CONTROL_ERROR = -1,
RECORD_RTMP_CONTROL_BUSY,
RECORD_RTMP_CONTROL_HANDLED,
RECORD_RTMP_CONTROL_OTHER
};
RETRO_BEGIN_DECLS
/* Single worker. Counts transport bytes once, including handshake bytes. */
record_rtmp_control_t *record_rtmp_control_new(void);
bool record_rtmp_control_received(record_rtmp_control_t *control, size_t size);
/* Complete messages only. BUSY takes nothing; caller retains the input.
* Applies receive chunk controls before the next input chunk is parsed. */
int record_rtmp_control_handle(record_rtmp_control_t *control,
record_rtmp_receive_t *receive, const record_rtmp_message_t *message);
/* Borrow response until ack, after all output bytes leave the transmitter.
* Short writes keep the same response; newly received bytes remain counted. */
bool record_rtmp_control_get(record_rtmp_control_t *control, record_rtmp_message_t *message);
bool record_rtmp_control_ack(record_rtmp_control_t *control);
bool record_rtmp_control_status(const record_rtmp_control_t *control,
record_rtmp_control_status_t *status);
bool record_rtmp_control_free(record_rtmp_control_t *control);
RETRO_END_DECLS
#endif
+129
View File
@@ -0,0 +1,129 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "record_rtmp_handshake.h"
#include <stdlib.h>
#include <string.h>
struct record_rtmp_handshake
{
size_t sent, received;
unsigned receive_stage;
uint8_t c1[1537], c2[1536];
bool failed;
};
static void handshake_time(uint8_t *p, uint32_t time)
{
p[0] = (uint8_t)(time >> 24);
p[1] = (uint8_t)(time >> 16);
p[2] = (uint8_t)(time >> 8);
p[3] = (uint8_t)time;
}
record_rtmp_handshake_t *record_rtmp_handshake_new(
const uint8_t random[1528], uint32_t time)
{
record_rtmp_handshake_t *h;
if (!random)
return NULL;
h = (record_rtmp_handshake_t*)calloc(1, sizeof(*h));
if (!h)
return NULL;
h->c1[0] = 3;
handshake_time(h->c1 + 1, time);
memcpy(h->c1 + 9, random, 1528);
return h;
}
bool record_rtmp_handshake_get(record_rtmp_handshake_t *h, record_rtmp_span_t *span)
{
if (!h || !span || h->failed)
return false;
if (h->sent < 1537)
{
span->data = h->c1 + h->sent;
span->size = 1537 - h->sent;
}
else if (h->receive_stage >= 2 && h->sent < 3073)
{
span->data = h->c2 + h->sent - 1537;
span->size = 3073 - h->sent;
}
else
return false;
return true;
}
bool record_rtmp_handshake_consume(record_rtmp_handshake_t *h, size_t size)
{
record_rtmp_span_t span;
if (!record_rtmp_handshake_get(h, &span) || size > span.size)
return false;
h->sent += size;
return true;
}
int record_rtmp_handshake_status(const record_rtmp_handshake_t *h)
{
if (!h || h->failed)
return RECORD_RTMP_HANDSHAKE_ERROR;
return h->sent == 3073 && h->receive_stage == 3
? RECORD_RTMP_HANDSHAKE_DONE : RECORD_RTMP_HANDSHAKE_PENDING;
}
int record_rtmp_handshake_feed(record_rtmp_handshake_t *h,
const void *data, size_t size, size_t *consumed, uint32_t time)
{
const uint8_t *p = (const uint8_t*)data;
size_t n, i, start;
if (consumed)
*consumed = 0;
if (!h || h->failed || !consumed || (!data && size))
return RECORD_RTMP_HANDSHAKE_ERROR;
if (!size || h->receive_stage == 3)
return record_rtmp_handshake_status(h);
if (!h->receive_stage)
{
*consumed = 1;
if (p[0] != 3)
goto failed;
h->receive_stage = 1;
return RECORD_RTMP_HANDSHAKE_PENDING;
}
n = size < 1536 - h->received ? size : 1536 - h->received;
if (h->receive_stage == 1)
{
memcpy(h->c2 + h->received, p, n);
h->received += n;
*consumed = n;
if (h->received == 1536)
{
if (h->c2[4] || h->c2[5] || h->c2[6] || h->c2[7])
goto failed;
handshake_time(h->c2 + 4, time);
h->received = 0;
h->receive_stage = 2;
}
}
else
{
start = h->received;
for (i = 0; i < n; i++)
{
h->received++;
*consumed = i + 1;
if ((start + i < 4 || start + i >= 8) && p[i] != h->c1[start + i + 1])
goto failed;
}
if (h->received == 1536)
h->receive_stage = 3;
}
return record_rtmp_handshake_status(h);
failed:
h->failed = true;
return RECORD_RTMP_HANDSHAKE_ERROR;
}
void record_rtmp_handshake_free(record_rtmp_handshake_t *h)
{
free(h);
}
+28
View File
@@ -0,0 +1,28 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef RECORD_RTMP_HANDSHAKE_H
#define RECORD_RTMP_HANDSHAKE_H
#include "record_rtmp_chunk.h"
typedef struct record_rtmp_handshake record_rtmp_handshake_t;
enum record_rtmp_handshake_result
{
RECORD_RTMP_HANDSHAKE_ERROR = -1,
RECORD_RTMP_HANDSHAKE_PENDING,
RECORD_RTMP_HANDSHAKE_DONE
};
RETRO_BEGIN_DECLS
/* Worker-owned version3 simple handshake. Copies 1528 caller-supplied random
* bytes at startup. Times are caller-supplied connection-relative milliseconds. */
record_rtmp_handshake_t *record_rtmp_handshake_new(
const uint8_t random[1528], uint32_t time);
bool record_rtmp_handshake_get(record_rtmp_handshake_t *handshake,
record_rtmp_span_t *span);
bool record_rtmp_handshake_consume(record_rtmp_handshake_t *handshake, size_t size);
/* Consumes at most one S0/S1/S2 segment per call. Leaves following chunk data
* untouched. No I/O, allocation, wait or internal clock. Errors are terminal. */
int record_rtmp_handshake_feed(record_rtmp_handshake_t *handshake,
const void *data, size_t size, size_t *consumed, uint32_t receive_time);
int record_rtmp_handshake_status(const record_rtmp_handshake_t *handshake);
void record_rtmp_handshake_free(record_rtmp_handshake_t *handshake);
RETRO_END_DECLS
#endif
+252
View File
@@ -0,0 +1,252 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "record_rtmp_publish.h"
#include <stdlib.h>
#include <string.h>
enum publish_wait { PUBLISH_CONNECT, PUBLISH_CREATE, PUBLISH_START };
struct record_rtmp_publish
{
record_rtmp_command_config_t config;
record_rtmp_reply_t *reply;
char *strings;
size_t output_size;
uint32_t stream, input_stream, create_transaction;
enum record_rtmp_publish_state state;
enum record_rtmp_publish_error error;
enum record_rtmp_command command;
enum publish_wait wait;
uint8_t output[9000];
bool pending, borrowed, input, legacy;
};
static bool publish_length(const char *s, size_t *length)
{
size_t n = 0;
if (!s || !*s)
return false;
while (n <= 4096 && s[n])
n++;
*length = n;
return n <= 4096;
}
static void publish_fail(record_rtmp_publish_t *p, enum record_rtmp_publish_error error)
{
if (p->state != RECORD_RTMP_PUBLISH_FAILED)
{
p->state = RECORD_RTMP_PUBLISH_FAILED;
p->error = error;
}
}
static bool publish_command(record_rtmp_publish_t *p, enum record_rtmp_command command)
{
if (!record_rtmp_command_encode(command, &p->config,
p->output, sizeof(p->output), &p->output_size))
{
publish_fail(p, RECORD_RTMP_PUBLISH_ERROR_PROTOCOL);
return false;
}
p->command = command;
p->pending = true;
return true;
}
record_rtmp_publish_t *record_rtmp_publish_new(const record_rtmp_publish_config_t *c)
{
record_rtmp_publish_t *p;
size_t app, url, name;
if (!c || !publish_length(c->application, &app) || !publish_length(c->url, &url)
|| !publish_length(c->name, &name))
return NULL;
p = (record_rtmp_publish_t*)calloc(1, sizeof(*p));
if (!p)
return NULL;
p->strings = (char*)malloc(app + url + name + 3);
p->reply = record_rtmp_reply_new();
if (!p->strings || !p->reply)
{
record_rtmp_publish_free(p);
return NULL;
}
memcpy(p->strings, c->application, app + 1);
memcpy(p->strings + app + 1, c->url, url + 1);
memcpy(p->strings + app + url + 2, c->name, name + 1);
p->config.application = p->strings;
p->config.url = p->strings + app + 1;
p->config.name = p->strings + app + url + 2;
p->legacy = c->legacy_commands;
p->create_transaction = p->legacy ? 4 : 2;
p->config.transaction = 1;
if (!publish_command(p, RECORD_RTMP_CONNECT))
{
record_rtmp_publish_free(p);
return NULL;
}
return p;
}
bool record_rtmp_publish_get(record_rtmp_publish_t *p, record_rtmp_message_t *message)
{
if (!p || !message || !p->pending || (p->state == RECORD_RTMP_PUBLISH_FAILED && !p->borrowed))
return false;
message->payload.data = p->output;
message->payload.size = p->output_size;
message->timestamp = 0;
message->stream_id = p->command == RECORD_RTMP_PUBLISH ? p->stream : 0;
message->chunk_id = 3;
message->type = 20;
p->borrowed = true;
return true;
}
bool record_rtmp_publish_ack(record_rtmp_publish_t *p)
{
if (!p || !p->pending || !p->borrowed)
return false;
p->pending = p->borrowed = false;
if (p->state == RECORD_RTMP_PUBLISH_FAILED)
return true;
switch (p->command)
{
case RECORD_RTMP_CONNECT:
p->wait = PUBLISH_CONNECT;
break;
case RECORD_RTMP_RELEASE_STREAM:
p->config.transaction = 3;
return publish_command(p, RECORD_RTMP_FC_PUBLISH);
case RECORD_RTMP_FC_PUBLISH:
p->config.transaction = p->create_transaction;
return publish_command(p, RECORD_RTMP_CREATE_STREAM);
case RECORD_RTMP_CREATE_STREAM:
p->wait = PUBLISH_CREATE;
break;
case RECORD_RTMP_PUBLISH:
p->wait = PUBLISH_START;
break;
default:
publish_fail(p, RECORD_RTMP_PUBLISH_ERROR_PROTOCOL);
return false;
}
return true;
}
bool record_rtmp_publish_reply(record_rtmp_publish_t *p, const record_rtmp_message_t *message)
{
if (!p || !message || p->input || p->pending || p->state == RECORD_RTMP_PUBLISH_FAILED
|| (message->type != 20 && message->type != 17))
return false;
if (!record_rtmp_reply_begin(p->reply, message->type, message->payload.data, message->payload.size))
{
publish_fail(p, RECORD_RTMP_PUBLISH_ERROR_PROTOCOL);
return false;
}
p->input_stream = message->stream_id;
p->input = true;
return true;
}
static bool publish_text(record_rtmp_span_t text, const char *value)
{
size_t size = strlen(value);
return text.size == size && !memcmp(text.data, value, size);
}
static void publish_transition(record_rtmp_publish_t *p, const record_rtmp_reply_info_t *r)
{
bool result = publish_text(r->command, "_result");
bool error = publish_text(r->command, "_error") || publish_text(r->level, "error");
uint32_t expected = p->wait == PUBLISH_CONNECT ? 1 : p->create_transaction;
if (p->state == RECORD_RTMP_PUBLISH_NEGOTIATING && p->wait != PUBLISH_START
&& !p->input_stream && r->transaction == expected)
{
if (error)
publish_fail(p, RECORD_RTMP_PUBLISH_ERROR_REJECTED);
else if (result && p->wait == PUBLISH_CONNECT)
{
if (!publish_text(r->code, "NetConnection.Connect.Success"))
publish_fail(p, RECORD_RTMP_PUBLISH_ERROR_PROTOCOL);
else
{
p->config.transaction = p->legacy ? 2 : p->create_transaction;
publish_command(p, p->legacy ? RECORD_RTMP_RELEASE_STREAM : RECORD_RTMP_CREATE_STREAM);
}
}
else if (result)
{
if (!r->stream_valid || !r->stream)
publish_fail(p, RECORD_RTMP_PUBLISH_ERROR_PROTOCOL);
else
{
p->stream = r->stream;
p->config.stream = r->stream;
publish_command(p, RECORD_RTMP_PUBLISH);
}
}
return;
}
if (p->stream && p->input_stream == p->stream && !r->transaction
&& (publish_text(r->command, "onStatus") || publish_text(r->command, "_error")))
{
if (error)
publish_fail(p, RECORD_RTMP_PUBLISH_ERROR_REJECTED);
else if (p->wait == PUBLISH_START && publish_text(r->code, "NetStream.Publish.Start"))
p->state = RECORD_RTMP_PUBLISH_READY;
}
}
int record_rtmp_publish_step(record_rtmp_publish_t *p)
{
record_rtmp_reply_info_t info;
int result;
if (!p)
return RECORD_RTMP_PUBLISH_ERROR;
if (p->input)
{
if (p->state != RECORD_RTMP_PUBLISH_FAILED)
{
result = record_rtmp_reply_step(p->reply);
if (result == RECORD_RTMP_REPLY_PROGRESS)
return RECORD_RTMP_PUBLISH_PROGRESS;
if (result == RECORD_RTMP_REPLY_ERROR || !record_rtmp_reply_get(p->reply, &info))
publish_fail(p, RECORD_RTMP_PUBLISH_ERROR_PROTOCOL);
else
publish_transition(p, &info);
}
record_rtmp_reply_release(p->reply);
p->input = false;
return RECORD_RTMP_PUBLISH_RELEASE_REPLY;
}
if (p->state == RECORD_RTMP_PUBLISH_FAILED)
return RECORD_RTMP_PUBLISH_ERROR;
return p->state == RECORD_RTMP_PUBLISH_READY ? RECORD_RTMP_PUBLISH_ESTABLISHED
: RECORD_RTMP_PUBLISH_BUSY;
}
enum record_rtmp_publish_state record_rtmp_publish_status(const record_rtmp_publish_t *p,
uint32_t *stream, enum record_rtmp_publish_error *error)
{
if (stream)
*stream = p ? p->stream : 0;
if (error)
*error = p ? p->error : RECORD_RTMP_PUBLISH_ERROR_PROTOCOL;
return p ? p->state : RECORD_RTMP_PUBLISH_FAILED;
}
void record_rtmp_publish_cancel(record_rtmp_publish_t *p)
{
if (p)
publish_fail(p, RECORD_RTMP_PUBLISH_ERROR_CANCELLED);
}
bool record_rtmp_publish_free(record_rtmp_publish_t *p)
{
if (!p)
return true;
if (p->borrowed || p->input)
return false;
record_rtmp_reply_free(p->reply);
free(p->strings);
free(p);
return true;
}
+55
View File
@@ -0,0 +1,55 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef RECORD_RTMP_PUBLISH_H
#define RECORD_RTMP_PUBLISH_H
#include "record_rtmp_command.h"
#include "record_rtmp_receive.h"
#include "record_rtmp_reply.h"
typedef struct record_rtmp_publish record_rtmp_publish_t;
typedef struct record_rtmp_publish_config
{
const char *application, *url, *name;
bool legacy_commands;
} record_rtmp_publish_config_t;
enum record_rtmp_publish_state
{
RECORD_RTMP_PUBLISH_NEGOTIATING,
RECORD_RTMP_PUBLISH_READY,
RECORD_RTMP_PUBLISH_FAILED
};
enum record_rtmp_publish_error
{
RECORD_RTMP_PUBLISH_ERROR_NONE,
RECORD_RTMP_PUBLISH_ERROR_PROTOCOL,
RECORD_RTMP_PUBLISH_ERROR_REJECTED,
RECORD_RTMP_PUBLISH_ERROR_CANCELLED
};
enum record_rtmp_publish_result
{
RECORD_RTMP_PUBLISH_ERROR = -1,
RECORD_RTMP_PUBLISH_BUSY,
RECORD_RTMP_PUBLISH_PROGRESS,
RECORD_RTMP_PUBLISH_RELEASE_REPLY,
RECORD_RTMP_PUBLISH_ESTABLISHED
};
RETRO_BEGIN_DECLS
/* Single worker, after handshake. Copies nonempty strings (up to4096 bytes
* each) at initialization. Does not own the socket, chunk sender or receiver. */
record_rtmp_publish_t *record_rtmp_publish_new(const record_rtmp_publish_config_t *config);
/* Borrows one outgoing command until ack, including across cancellation.
* Ack only after transmission completes or its transport borrow is retired. */
bool record_rtmp_publish_get(record_rtmp_publish_t *publish, record_rtmp_message_t *message);
bool record_rtmp_publish_ack(record_rtmp_publish_t *publish);
/* Complete command replies only. True borrows the receiver message until
* step returns RELEASE_REPLY, including errors/cancellation. False takes none. */
bool record_rtmp_publish_reply(record_rtmp_publish_t *publish, const record_rtmp_message_t *message);
/* One AMF parsing step or one reply transition. BUSY requires external I/O.
* No I/O, allocation, wait or clock here. Control messages are handled outside. */
int record_rtmp_publish_step(record_rtmp_publish_t *publish);
enum record_rtmp_publish_state record_rtmp_publish_status(const record_rtmp_publish_t *publish,
uint32_t *stream, enum record_rtmp_publish_error *error);
void record_rtmp_publish_cancel(record_rtmp_publish_t *publish);
/* Refuses outstanding input/output borrows. Unborrowed pending output is safe. */
bool record_rtmp_publish_free(record_rtmp_publish_t *publish);
RETRO_END_DECLS
#endif
+293
View File
@@ -0,0 +1,293 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "record_rtmp_receive.h"
#include <stdlib.h>
#include <string.h>
struct receive_stream
{
size_t used;
uint32_t timestamp, delta, stamp, length, stream;
unsigned id, type;
bool valid, active, extended;
};
enum receive_stage { RECEIVE_BASIC, RECEIVE_ID, RECEIVE_HEADER, RECEIVE_EXTENDED, RECEIVE_PAYLOAD };
struct record_rtmp_receive
{
struct receive_stream *streams;
uint8_t *storage;
size_t limit, have, need, left;
unsigned count, chunk_size, id, fmt, current, ready;
enum receive_stage stage;
uint8_t header[11], extension[4];
bool failed, finished;
};
static uint32_t receive_be(const uint8_t *p, unsigned n)
{
uint32_t value = 0;
unsigned i;
for (i = 0; i < n; i++)
value = (value << 8) | p[i];
return value;
}
record_rtmp_receive_t *record_rtmp_receive_new(size_t limit, unsigned streams)
{
record_rtmp_receive_t *r;
if (!limit || limit > 0xffffff || !streams || streams > 64
|| limit > (size_t)-1 / streams)
return NULL;
r = (record_rtmp_receive_t*)calloc(1, sizeof(*r));
if (!r)
return NULL;
r->streams = (struct receive_stream*)calloc(streams, sizeof(*r->streams));
r->storage = (uint8_t*)malloc(limit * streams);
if (!r->streams || !r->storage)
{
record_rtmp_receive_free(r);
return NULL;
}
r->limit = limit;
r->count = streams;
r->chunk_size = 128;
return r;
}
static bool receive_prepare(record_rtmp_receive_t *r, uint32_t stamp);
static bool receive_select(record_rtmp_receive_t *r)
{
unsigned i, empty = r->count;
for (i = 0; i < r->count; i++)
{
if (r->streams[i].id == r->id)
break;
if (!r->streams[i].id)
empty = i;
}
if (i == r->count)
{
if (empty == r->count || r->fmt)
return false;
i = empty;
r->streams[i].id = r->id;
}
if (r->fmt && !r->streams[i].valid)
return false;
r->current = i;
r->have = 0;
r->need = r->fmt == 0 ? 11 : r->fmt == 1 ? 7 : r->fmt == 2 ? 3 : 0;
r->stage = RECEIVE_HEADER;
if (!r->need)
{
if (!r->streams[i].extended)
return receive_prepare(r, r->streams[i].stamp);
r->stage = RECEIVE_EXTENDED;
r->need = 4;
}
return true;
}
static bool receive_prepare(record_rtmp_receive_t *r, uint32_t stamp)
{
struct receive_stream *s = r->streams + r->current;
uint32_t length = s->length, stream = s->stream, timestamp, delta;
unsigned type = s->type;
if (r->fmt <= 1)
{
length = receive_be(r->header + 3, 3);
type = r->header[6];
}
if (!r->fmt)
stream = (uint32_t)r->header[7] | ((uint32_t)r->header[8] << 8)
| ((uint32_t)r->header[9] << 16) | ((uint32_t)r->header[10] << 24);
delta = r->fmt == 3 ? s->delta : stamp;
timestamp = !r->fmt ? stamp : s->active ? s->timestamp : s->timestamp + delta;
if (length > r->limit || !type || (s->active &&
(length != s->length || type != s->type || stream != s->stream
|| timestamp != s->timestamp || (r->fmt && r->fmt != 3 && stamp != s->delta))))
return false;
if (!s->active)
s->used = 0;
s->length = length;
s->stream = stream;
s->timestamp = timestamp;
s->delta = delta;
s->type = type;
s->valid = s->active = true;
if (r->fmt != 3)
s->stamp = stamp;
r->left = length - s->used;
if (r->left > r->chunk_size)
r->left = r->chunk_size;
r->stage = RECEIVE_PAYLOAD;
if (!length)
{
s->active = false;
r->ready = r->current + 1;
r->stage = RECEIVE_BASIC;
}
return true;
}
int record_rtmp_receive_feed(record_rtmp_receive_t *r,
const void *data, size_t size, size_t *consumed)
{
const uint8_t *p = (const uint8_t*)data;
struct receive_stream *s;
size_t n;
uint32_t stamp;
if (consumed)
*consumed = 0;
if (!r || r->failed || r->finished || !consumed || (!data && size))
return RECORD_RTMP_RECEIVE_ERROR;
if (r->ready)
return RECORD_RTMP_RECEIVE_MESSAGE;
if (!size)
return RECORD_RTMP_RECEIVE_MORE;
if (r->stage == RECEIVE_BASIC)
{
r->fmt = p[0] >> 6;
r->id = p[0] & 63;
*consumed = 1;
if (r->id >= 2)
{
if (!receive_select(r))
goto failed;
}
else
{
r->stage = RECEIVE_ID;
r->have = 0;
r->need = r->id ? 2 : 1;
}
return r->ready ? RECORD_RTMP_RECEIVE_MESSAGE : RECORD_RTMP_RECEIVE_MORE;
}
if (r->stage == RECEIVE_PAYLOAD)
{
s = r->streams + r->current;
n = size < r->left ? size : r->left;
if (n > 4096)
n = 4096;
memcpy(r->storage + r->limit * r->current + s->used, p, n);
s->used += n;
r->left -= n;
*consumed = n;
if (!r->left)
r->stage = RECEIVE_BASIC;
if (s->used == s->length)
{
s->active = false;
r->ready = r->current + 1;
return RECORD_RTMP_RECEIVE_MESSAGE;
}
return RECORD_RTMP_RECEIVE_MORE;
}
n = size < r->need - r->have ? size : r->need - r->have;
memcpy((r->stage == RECEIVE_EXTENDED ? r->extension : r->header) + r->have, p, n);
r->have += n;
*consumed = n;
if (r->have != r->need)
return RECORD_RTMP_RECEIVE_MORE;
if (r->stage == RECEIVE_ID)
{
r->id = 64 + r->header[0] + (r->need == 2 ? (unsigned)r->header[1] * 256 : 0);
if (!receive_select(r))
goto failed;
return r->ready ? RECORD_RTMP_RECEIVE_MESSAGE : RECORD_RTMP_RECEIVE_MORE;
}
s = r->streams + r->current;
if (r->stage == RECEIVE_HEADER)
{
stamp = r->fmt == 3 ? s->stamp : receive_be(r->header, 3);
if (r->fmt != 3)
s->extended = stamp == 0xffffff;
if (s->extended)
{
r->stage = RECEIVE_EXTENDED;
r->have = 0;
r->need = 4;
return RECORD_RTMP_RECEIVE_MORE;
}
}
else
stamp = receive_be(r->extension, 4);
if (r->fmt == 3 && stamp != s->stamp)
goto failed;
if (!receive_prepare(r, stamp))
goto failed;
return r->ready ? RECORD_RTMP_RECEIVE_MESSAGE : RECORD_RTMP_RECEIVE_MORE;
failed:
r->failed = true;
return RECORD_RTMP_RECEIVE_ERROR;
}
bool record_rtmp_receive_get(record_rtmp_receive_t *r, record_rtmp_message_t *message)
{
struct receive_stream *s;
if (!r || r->failed || !r->ready || !message)
return false;
s = r->streams + r->ready - 1;
message->payload.data = r->storage + r->limit * (r->ready - 1);
message->payload.size = s->length;
message->timestamp = s->timestamp;
message->stream_id = s->stream;
message->chunk_id = s->id;
message->type = s->type;
return true;
}
bool record_rtmp_receive_release(record_rtmp_receive_t *r)
{
if (!r || r->failed || !r->ready)
return false;
r->ready = 0;
return true;
}
bool record_rtmp_receive_size(record_rtmp_receive_t *r, unsigned size)
{
if (!r || r->failed || r->finished || r->stage != RECEIVE_BASIC || !size || size > 0x7fffffff)
return false;
r->chunk_size = size;
return true;
}
bool record_rtmp_receive_abort(record_rtmp_receive_t *r, unsigned id)
{
unsigned i;
if (!r || r->failed || r->finished || r->stage != RECEIVE_BASIC || id < 2 || id > 65599)
return false;
for (i = 0; i < r->count; i++)
if (r->streams[i].id == id)
{
if (r->ready == i + 1)
return false;
r->streams[i].active = false;
r->streams[i].used = 0;
break;
}
return true;
}
bool record_rtmp_receive_finish(record_rtmp_receive_t *r)
{
unsigned i;
if (!r || r->failed || r->ready || r->stage != RECEIVE_BASIC)
return false;
for (i = 0; i < r->count; i++)
if (r->streams[i].active)
return false;
r->finished = true;
return true;
}
void record_rtmp_receive_free(record_rtmp_receive_t *r)
{
if (!r)
return;
free(r->streams);
free(r->storage);
free(r);
}
+37
View File
@@ -0,0 +1,37 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef RECORD_RTMP_RECEIVE_H
#define RECORD_RTMP_RECEIVE_H
#include "record_rtmp_chunk.h"
typedef struct record_rtmp_receive record_rtmp_receive_t;
typedef struct record_rtmp_message
{
record_rtmp_span_t payload;
uint32_t timestamp, stream_id;
unsigned chunk_id, type;
} record_rtmp_message_t;
enum record_rtmp_receive_result
{
RECORD_RTMP_RECEIVE_ERROR = -1,
RECORD_RTMP_RECEIVE_MORE,
RECORD_RTMP_RECEIVE_MESSAGE
};
RETRO_BEGIN_DECLS
/* Worker-owned. Preallocates per-stream message storage; 1..64 cached streams.
* Oversized messages or too many distinct streams fail rather than allocate.
* Initial incoming chunk size is 128. */
record_rtmp_receive_t *record_rtmp_receive_new(size_t max_message, unsigned streams);
/* One header segment or at most 4096 payload bytes per call. Pending output
* consumes nothing until released. Protocol errors are sticky. No I/O/wait. */
int record_rtmp_receive_feed(record_rtmp_receive_t *receive,
const void *data, size_t size, size_t *consumed);
bool record_rtmp_receive_get(record_rtmp_receive_t *receive, record_rtmp_message_t *message);
bool record_rtmp_receive_release(record_rtmp_receive_t *receive);
/* Apply validated Set Chunk Size / Abort controls at a chunk boundary. */
bool record_rtmp_receive_size(record_rtmp_receive_t *receive, unsigned size);
bool record_rtmp_receive_abort(record_rtmp_receive_t *receive, unsigned chunk_id);
/* End of input succeeds only after all complete output is acknowledged. */
bool record_rtmp_receive_finish(record_rtmp_receive_t *receive);
void record_rtmp_receive_free(record_rtmp_receive_t *receive);
RETRO_END_DECLS
#endif
+272
View File
@@ -0,0 +1,272 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "record_rtmp_reply.h"
#include <stdlib.h>
#include <string.h>
struct reply_container
{
record_rtmp_span_t key;
uint32_t remaining;
unsigned root;
bool named, value_pending;
};
struct record_rtmp_reply
{
record_rtmp_reply_info_t info;
struct reply_container stack[8];
const uint8_t *data;
size_t size, offset;
unsigned depth, roots, objects, fields;
bool failed, done;
};
static bool reply_take(record_rtmp_reply_t *r, size_t size, const uint8_t **data)
{
if (size > r->size - r->offset)
return false;
*data = r->data + r->offset;
r->offset += size;
return true;
}
static uint32_t reply_uint(const uint8_t *p, unsigned size)
{
uint32_t value = 0;
unsigned i;
for (i = 0; i < size; i++)
value = (value << 8) | p[i];
return value;
}
static bool reply_text(record_rtmp_reply_t *r, unsigned width, record_rtmp_span_t *text)
{
const uint8_t *p;
uint32_t size;
if (!reply_take(r, width, &p))
return false;
size = reply_uint(p, width);
if (!reply_take(r, size, &p))
return false;
text->data = p;
text->size = size;
return true;
}
static bool reply_name(record_rtmp_span_t key, const char *name, size_t size)
{
return key.size == size && !memcmp(key.data, name, size);
}
static bool reply_integer(const uint8_t *p, uint32_t *value)
{
uint64_t bits = ((uint64_t)reply_uint(p, 4) << 32) | reply_uint(p + 4, 4);
uint64_t fraction = bits & (((uint64_t)1 << 52) - 1), mantissa;
unsigned exponent = (unsigned)((bits >> 52) & 2047), shift;
if (!exponent && !fraction)
{
*value = 0;
return true;
}
if ((bits >> 63) || exponent < 1023 || exponent > 1054)
return false;
shift = 52 - (exponent - 1023);
mantissa = ((uint64_t)1 << 52) | fraction;
if (mantissa & (((uint64_t)1 << shift) - 1))
return false;
*value = (uint32_t)(mantissa >> shift);
return true;
}
record_rtmp_reply_t *record_rtmp_reply_new(void)
{
return (record_rtmp_reply_t*)calloc(1, sizeof(record_rtmp_reply_t));
}
bool record_rtmp_reply_begin(record_rtmp_reply_t *r, unsigned type, const void *data, size_t size)
{
const uint8_t *p = (const uint8_t*)data;
if (!r || r->data || !data || !size || size > 0xffffff
|| (type != 20 && type != 17) || (type == 17 && (size == 1 || p[0])))
return false;
r->data = p;
r->size = size;
r->offset = type == 17 ? 1 : 0;
return true;
}
int record_rtmp_reply_step(record_rtmp_reply_t *r)
{
struct reply_container *parent = NULL, *child;
record_rtmp_span_t text, key;
const uint8_t *p;
unsigned root, marker, field = 0;
uint32_t value = 0, count = 0;
bool named;
key.data = NULL;
key.size = 0;
if (!r || !r->data || r->failed)
return RECORD_RTMP_REPLY_ERROR;
if (r->done)
return RECORD_RTMP_REPLY_DONE;
if (r->depth)
{
parent = r->stack + r->depth - 1;
if (!parent->named && !parent->remaining)
{
r->depth--;
return RECORD_RTMP_REPLY_PROGRESS;
}
if (parent->named && !parent->value_pending)
{
if (r->size - r->offset >= 3 && !memcmp(r->data + r->offset, "\0\0\11", 3))
{
r->offset += 3;
r->depth--;
return RECORD_RTMP_REPLY_PROGRESS;
}
if (!reply_text(r, 2, &parent->key))
goto failed;
parent->value_pending = true;
return RECORD_RTMP_REPLY_PROGRESS;
}
}
else if (r->offset == r->size)
{
/* Legacy publishers may receive a name-only callback. */
if (r->roots < 3 && !(r->roots == 1
&& reply_name(r->info.command, "onFCPublish", 11)))
goto failed;
r->done = true;
return RECORD_RTMP_REPLY_DONE;
}
if (!reply_take(r, 1, &p))
goto failed;
marker = p[0];
root = parent ? parent->root : r->roots++;
if (parent)
{
if (parent->named)
{
key = parent->key;
parent->value_pending = false;
}
else
parent->remaining--;
}
if (!r->depth && ((root == 0 && marker != 2 && marker != 12)
|| (root == 1 && marker != 0)))
goto failed;
if (r->depth == 1 && root == 3 && parent->named)
{
if (reply_name(key, "code", 4))
field = 1;
else if (reply_name(key, "level", 5))
field = 2;
else if (reply_name(key, "description", 11))
field = 4;
if (field && ((r->fields & field) || (marker != 2 && marker != 12)))
goto failed;
}
switch (marker)
{
case 0:
if (!reply_take(r, 8, &p))
goto failed;
if (!r->depth && (root == 1 || root == 3))
{
if (!reply_integer(p, &value))
goto failed;
if (root == 1)
r->info.transaction = value;
else
{
r->info.stream = value;
r->info.stream_valid = true;
}
}
break;
case 1:
if (!reply_take(r, 1, &p))
goto failed;
break;
case 2:
case 12:
case 15:
if (!reply_text(r, marker == 2 ? 2 : 4, &text))
goto failed;
if (!r->depth && root == 0)
r->info.command = text;
if (field)
{
r->fields |= field;
if (field == 1)
r->info.code = text;
else if (field == 2)
r->info.level = text;
else
r->info.description = text;
}
break;
case 5:
case 6:
case 13:
break;
case 7:
if (!reply_take(r, 2, &p) || reply_uint(p, 2) >= r->objects)
goto failed;
break;
case 11:
if (!reply_take(r, 10, &p))
goto failed;
break;
case 3:
case 8:
case 10:
case 16:
if (r->depth == 8)
goto failed;
named = marker != 10;
if (marker == 8 || marker == 10)
{
if (!reply_take(r, 4, &p))
goto failed;
count = reply_uint(p, 4);
if (!named && count > r->size - r->offset)
goto failed;
}
if (marker == 16 && !reply_text(r, 2, &text))
goto failed;
r->objects++;
child = r->stack + r->depth++;
child->root = root;
child->named = named;
child->value_pending = false;
child->remaining = count;
break;
default:
goto failed;
}
return RECORD_RTMP_REPLY_PROGRESS;
failed:
r->failed = true;
return RECORD_RTMP_REPLY_ERROR;
}
bool record_rtmp_reply_get(record_rtmp_reply_t *r, record_rtmp_reply_info_t *info)
{
if (!r || !r->done || r->failed || !info)
return false;
*info = r->info;
return true;
}
void record_rtmp_reply_release(record_rtmp_reply_t *r)
{
if (r)
memset(r, 0, sizeof(*r));
}
void record_rtmp_reply_free(record_rtmp_reply_t *r)
{
free(r);
}
+34
View File
@@ -0,0 +1,34 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef RECORD_RTMP_REPLY_H
#define RECORD_RTMP_REPLY_H
#include "record_rtmp_chunk.h"
typedef struct record_rtmp_reply record_rtmp_reply_t;
typedef struct record_rtmp_reply_info
{
record_rtmp_span_t command, code, level, description;
uint32_t transaction, stream;
bool stream_valid;
} record_rtmp_reply_info_t;
enum record_rtmp_reply_result
{
RECORD_RTMP_REPLY_ERROR = -1,
RECORD_RTMP_REPLY_PROGRESS,
RECORD_RTMP_REPLY_DONE
};
RETRO_BEGIN_DECLS
record_rtmp_reply_t *record_rtmp_reply_new(void);
/* Borrows a complete command message until release/free. Type20 is AMF0;
* type17 accepts format selector0 with AMF0 values. AMF3 values are rejected. */
bool record_rtmp_reply_begin(record_rtmp_reply_t *reply, unsigned type,
const void *data, size_t size);
/* One property, value or container close per step; nesting is limited to8.
* No allocation, I/O or wait. Strings remain byte spans, not C strings. */
int record_rtmp_reply_step(record_rtmp_reply_t *reply);
/* Available only after DONE. Status fields come only from the top-level
* fourth argument object; nested lookalikes cannot replace them. */
bool record_rtmp_reply_get(record_rtmp_reply_t *reply, record_rtmp_reply_info_t *info);
void record_rtmp_reply_release(record_rtmp_reply_t *reply);
void record_rtmp_reply_free(record_rtmp_reply_t *reply);
RETRO_END_DECLS
#endif
+239
View File
@@ -0,0 +1,239 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "record_rtmp_session.h"
#include <stdlib.h>
enum session_output { SESSION_OUTPUT_NONE, SESSION_OUTPUT_CONTROL, SESSION_OUTPUT_PUBLISH };
struct record_rtmp_session
{
record_rtmp_handshake_t *handshake;
record_rtmp_publish_t *publish;
record_rtmp_control_t *control;
record_rtmp_receive_t *receive;
record_rtmp_chunk_t *chunk;
record_rtmp_span_t view;
uint64_t sent;
enum record_rtmp_session_state state;
enum session_output owner;
bool borrowed, handshake_view, reply;
};
record_rtmp_session_t *record_rtmp_session_new(const record_rtmp_publish_config_t *config,
const uint8_t random[1528], uint32_t time, size_t max_reply, unsigned streams)
{
record_rtmp_session_t *s = (record_rtmp_session_t*)calloc(1, sizeof(*s));
if (!s)
return NULL;
s->handshake = record_rtmp_handshake_new(random, time);
s->publish = record_rtmp_publish_new(config);
s->control = record_rtmp_control_new();
s->receive = record_rtmp_receive_new(max_reply, streams);
s->chunk = record_rtmp_chunk_new(128);
if (!s->handshake || !s->publish || !s->control || !s->receive || !s->chunk)
{
record_rtmp_session_free(s);
return NULL;
}
return s;
}
bool record_rtmp_session_feed(record_rtmp_session_t *s,
const void *data, size_t size, size_t *used, uint32_t time)
{
int result;
if (!used)
return false;
*used = 0;
if (!s || s->state == RECORD_RTMP_SESSION_FAILED || (size && !data))
return false;
if (s->state == RECORD_RTMP_SESSION_HANDSHAKE)
result = record_rtmp_handshake_feed(s->handshake, data, size, used, time);
else
result = record_rtmp_receive_feed(s->receive, data, size, used);
if (result < 0 || !record_rtmp_control_received(s->control, *used))
{
s->state = RECORD_RTMP_SESSION_FAILED;
return false;
}
return true;
}
bool record_rtmp_session_step(record_rtmp_session_t *s)
{
record_rtmp_span_t span;
record_rtmp_message_t m;
int result;
if (!s || s->state == RECORD_RTMP_SESSION_FAILED)
return false;
if (s->state == RECORD_RTMP_SESSION_HANDSHAKE)
{
result = record_rtmp_handshake_status(s->handshake);
if (result == RECORD_RTMP_HANDSHAKE_ERROR)
goto invalid;
if (result != RECORD_RTMP_HANDSHAKE_DONE)
return false;
s->state = RECORD_RTMP_SESSION_NEGOTIATING;
return true;
}
if (s->owner != SESSION_OUTPUT_NONE && !record_rtmp_chunk_get(s->chunk, &span))
{
result = s->owner == SESSION_OUTPUT_CONTROL
? record_rtmp_control_ack(s->control) : record_rtmp_publish_ack(s->publish);
s->owner = SESSION_OUTPUT_NONE;
if (!result)
goto invalid;
return true;
}
if (s->reply)
{
result = record_rtmp_publish_step(s->publish);
if (result == RECORD_RTMP_PUBLISH_RELEASE_REPLY)
{
s->reply = false;
if (!record_rtmp_receive_release(s->receive))
goto invalid;
if (record_rtmp_publish_status(s->publish, NULL, NULL) == RECORD_RTMP_PUBLISH_FAILED)
goto invalid;
if (record_rtmp_publish_status(s->publish, NULL, NULL) == RECORD_RTMP_PUBLISH_READY)
s->state = RECORD_RTMP_SESSION_READY;
}
else if (result != RECORD_RTMP_PUBLISH_PROGRESS)
goto invalid;
return true;
}
if (record_rtmp_receive_get(s->receive, &m))
{
result = record_rtmp_control_handle(s->control, s->receive, &m);
if (result == RECORD_RTMP_CONTROL_ERROR)
goto invalid;
if (result == RECORD_RTMP_CONTROL_HANDLED)
{
if (!record_rtmp_receive_release(s->receive))
goto invalid;
return true;
}
if (result == RECORD_RTMP_CONTROL_OTHER)
{
if (m.type != 17 && m.type != 20)
{
if (!record_rtmp_receive_release(s->receive))
goto invalid;
return true;
}
if (s->owner == SESSION_OUTPUT_NONE)
{
/* Pending commands must leave before their replies are admitted. */
if (!record_rtmp_publish_get(s->publish, &m))
{
if (!record_rtmp_receive_get(s->receive, &m)
|| !record_rtmp_publish_reply(s->publish, &m))
goto invalid;
s->reply = true;
return true;
}
/* get borrowed a pending command; stage it below. */
s->owner = SESSION_OUTPUT_PUBLISH;
if (!record_rtmp_chunk_push(s->chunk, m.chunk_id, m.stream_id,
m.type, m.timestamp, &m.payload, 1))
goto invalid;
return true;
}
}
}
if (s->owner != SESSION_OUTPUT_NONE)
return false;
if (record_rtmp_control_get(s->control, &m))
s->owner = SESSION_OUTPUT_CONTROL;
else if (record_rtmp_publish_get(s->publish, &m))
s->owner = SESSION_OUTPUT_PUBLISH;
else if (record_rtmp_publish_status(s->publish, NULL, NULL) == RECORD_RTMP_PUBLISH_FAILED)
goto invalid;
else
return false;
if (!record_rtmp_chunk_push(s->chunk, m.chunk_id, m.stream_id,
m.type, m.timestamp, &m.payload, 1))
goto invalid;
return true;
invalid:
s->state = RECORD_RTMP_SESSION_FAILED;
return false;
}
bool record_rtmp_session_get(record_rtmp_session_t *s, record_rtmp_span_t *span)
{
if (!s || !span)
return false;
if (!s->borrowed)
{
if (s->state == RECORD_RTMP_SESSION_FAILED)
return false;
s->handshake_view = s->state == RECORD_RTMP_SESSION_HANDSHAKE;
if (!(s->handshake_view ? record_rtmp_handshake_get(s->handshake, &s->view)
: record_rtmp_chunk_get(s->chunk, &s->view)))
return false;
s->borrowed = true;
}
*span = s->view;
return true;
}
bool record_rtmp_session_consume(record_rtmp_session_t *s, size_t size)
{
if (!s || !s->borrowed || size > s->view.size
|| (uint64_t)size > (uint64_t)-1 - s->sent)
return false;
if (!(s->handshake_view ? record_rtmp_handshake_consume(s->handshake, size)
: record_rtmp_chunk_consume(s->chunk, size)))
return false;
s->sent += size;
s->view.data = (const uint8_t*)s->view.data + size;
s->view.size -= size;
if (!s->view.size)
s->borrowed = false;
return true;
}
enum record_rtmp_session_state record_rtmp_session_status(
const record_rtmp_session_t *s, uint32_t *stream, uint64_t *sent)
{
if (stream)
record_rtmp_publish_status(s ? s->publish : NULL, stream, NULL);
if (sent)
*sent = s ? s->sent : 0;
return s ? s->state : RECORD_RTMP_SESSION_FAILED;
}
void record_rtmp_session_cancel(record_rtmp_session_t *s)
{
if (s)
s->state = RECORD_RTMP_SESSION_FAILED;
}
bool record_rtmp_session_discard(record_rtmp_session_t *s)
{
if (!s || s->state != RECORD_RTMP_SESSION_FAILED)
return false;
s->borrowed = false;
return true;
}
bool record_rtmp_session_free(record_rtmp_session_t *s)
{
if (!s)
return true;
if (s->borrowed)
return false;
record_rtmp_chunk_free(s->chunk);
record_rtmp_publish_cancel(s->publish);
if (s->reply)
record_rtmp_publish_step(s->publish);
if (s->owner == SESSION_OUTPUT_PUBLISH)
record_rtmp_publish_ack(s->publish);
if (s->owner == SESSION_OUTPUT_CONTROL)
record_rtmp_control_ack(s->control);
record_rtmp_publish_free(s->publish);
record_rtmp_control_free(s->control);
record_rtmp_receive_free(s->receive);
record_rtmp_handshake_free(s->handshake);
free(s);
return true;
}
+38
View File
@@ -0,0 +1,38 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef RECORD_RTMP_SESSION_H
#define RECORD_RTMP_SESSION_H
#include "record_rtmp_publish.h"
#include "record_rtmp_control.h"
#include "record_rtmp_handshake.h"
typedef struct record_rtmp_session record_rtmp_session_t;
enum record_rtmp_session_state
{
RECORD_RTMP_SESSION_FAILED = -1,
RECORD_RTMP_SESSION_HANDSHAKE,
RECORD_RTMP_SESSION_NEGOTIATING,
RECORD_RTMP_SESSION_READY
};
RETRO_BEGIN_DECLS
/* One worker; cold allocation. Copies configuration and handshake random.
* No socket ownership, internal clock or I/O. */
record_rtmp_session_t *record_rtmp_session_new(const record_rtmp_publish_config_t *config,
const uint8_t random[1528], uint32_t time, size_t max_reply, unsigned streams);
/* One handshake/receive segment. Zero consumption requires step/output work. */
bool record_rtmp_session_feed(record_rtmp_session_t *session,
const void *data, size_t size, size_t *consumed, uint32_t receive_time);
/* One output retirement, command dispatch or reply-parser step. True means
* local progress; false requires transport activity or reports FAILED. */
bool record_rtmp_session_step(record_rtmp_session_t *session);
/* Borrow one output span. Partial consumption keeps its remaining bytes live. */
bool record_rtmp_session_get(record_rtmp_session_t *session, record_rtmp_span_t *span);
bool record_rtmp_session_consume(record_rtmp_session_t *session, size_t size);
enum record_rtmp_session_state record_rtmp_session_status(
const record_rtmp_session_t *session, uint32_t *stream, uint64_t *sent);
void record_rtmp_session_cancel(record_rtmp_session_t *session);
/* After cancellation, retire a transport borrow without reporting it sent. */
bool record_rtmp_session_discard(record_rtmp_session_t *session);
/* Refuses a live transport borrow. Releases internal borrows before freeing. */
bool record_rtmp_session_free(record_rtmp_session_t *session);
RETRO_END_DECLS
#endif
+19
View File
@@ -0,0 +1,19 @@
CC ?= cc
RA := ../../..
CFLAGS += -O1 -g1 -std=c89 -pedantic -Wall -Wextra -Werror -Wframe-larger-than=2048 -I$(RA)/libretro-common/include
ifneq ($(SANITIZER),)
CFLAGS += -fsanitize=$(SANITIZER) -fno-omit-frame-pointer
LDFLAGS += -fsanitize=$(SANITIZER)
endif
all: flv_mux_test$(EXE) flv_remux_test$(EXE)
flv_mux_test$(EXE): flv_mux_test.c $(RA)/record/record_mux_flv.c $(RA)/record/record_mux_flv.h
$(CC) $(CFLAGS) -o $@ flv_mux_test.c $(RA)/record/record_mux_flv.c $(LDFLAGS)
flv_remux_test$(EXE): flv_remux_test.c $(RA)/record/record_mux_flv.c $(RA)/record/record_mux_flv.h
$(CC) $(CFLAGS) -o $@ flv_remux_test.c $(RA)/record/record_mux_flv.c $(LDFLAGS)
check: all
$(RUNNER) ./flv_mux_test$(EXE)
stream-check: all
python3 run.py --runner "$(RUNNER)" ./flv_remux_test$(EXE)
clean:
rm -f flv_mux_test flv_mux_test.exe flv_remux_test flv_remux_test.exe
.PHONY: all check stream-check clean
+213
View File
@@ -0,0 +1,213 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "../../../record/record_mux_flv.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
static const unsigned char avcc[] =
{ 1, 66, 0, 30, 255, 225, 0, 4, 103, 66, 0, 30, 1, 0, 1, 104 };
static const unsigned char asc[] = { 18, 16 };
static const unsigned char idr[] =
{ 0, 0, 0, 2, 101, 136, 0, 0, 0, 2, 6, 128 };
static const unsigned char predicted[] = { 0, 0, 0, 2, 65, 154 };
static record_flv_config_t config(void)
{
record_flv_config_t c;
memset(&c, 0, sizeof(c));
c.avcc = avcc;
c.avcc_size = sizeof(avcc);
c.asc = asc;
c.asc_size = sizeof(asc);
c.video_timescale = 90000;
c.audio_timescale = 44100;
c.max_packet_size = 1024;
return c;
}
static unsigned be(const void *data, unsigned count)
{
const unsigned char *p = (const unsigned char*)data;
unsigned value = 0, i;
for (i = 0; i < count; i++)
value = (value << 8) | p[i];
return value;
}
static void headers(record_mux_flv_t *m, unsigned flags)
{
const record_flv_span_t *s;
const unsigned char *p;
size_t n;
assert(record_mux_flv_get(m, &s, &n) && n == 1);
p = (const unsigned char*)s[0].data;
assert(s[0].size == 13 && !memcmp(p, "FLV\1", 4) && p[4] == flags);
assert(be(p + 5, 4) == 9 && be(p + 9, 4) == 0);
assert(!record_mux_flv_finish(m));
assert(record_mux_flv_push(m, 1, idr, sizeof(idr), 0, 0, true) == RECORD_FLV_BUSY);
assert(record_mux_flv_release(m));
if (flags & 1)
{
assert(record_mux_flv_get(m, &s, &n) && n == 3);
p = (const unsigned char*)s[0].data;
assert(p[0] == 9 && p[11] == 23 && p[12] == 0);
assert(be(p + 1, 3) == sizeof(avcc) + 5);
assert(s[1].size == sizeof(avcc) && !memcmp(s[1].data, avcc, sizeof(avcc)));
assert(be(s[2].data, 4) == sizeof(avcc) + 16);
assert(record_mux_flv_release(m));
}
if (flags & 4)
{
assert(record_mux_flv_get(m, &s, &n) && n == 3);
p = (const unsigned char*)s[0].data;
assert(p[0] == 8 && p[11] == 175 && p[12] == 0);
assert(s[1].size == sizeof(asc) && !memcmp(s[1].data, asc, sizeof(asc)));
assert(record_mux_flv_release(m));
}
assert(record_mux_flv_step(m) == RECORD_FLV_BUSY);
}
static void lifetime(void)
{
record_flv_config_t c = config();
unsigned char owned[sizeof(avcc)];
record_mux_flv_t *m;
const record_flv_span_t *s;
const unsigned char *p;
size_t n;
memcpy(owned, avcc, sizeof(owned));
c.avcc = owned;
m = record_mux_flv_new(&c);
assert(m);
memset(owned, 0, sizeof(owned));
headers(m, 5);
assert(record_mux_flv_push(m, 1, predicted, sizeof(predicted), 0, 0, false) == RECORD_FLV_ERROR);
assert(record_mux_flv_push(m, 1, idr, sizeof(idr), 90000, 81000, true) == RECORD_FLV_PROGRESS);
assert(!record_mux_flv_finish(m));
assert(record_mux_flv_step(m) == RECORD_FLV_PROGRESS);
assert(record_mux_flv_step(m) == RECORD_FLV_PROGRESS);
assert(record_mux_flv_step(m) == RECORD_FLV_PACKET);
assert(record_mux_flv_get(m, &s, &n) && n == 3 && s[1].data == idr);
p = (const unsigned char*)s[0].data;
assert(be(p + 4, 3) == 1000 && be(p + 13, 3) == 0xffff9c);
assert(record_mux_flv_step(m) == RECORD_FLV_PACKET);
assert(record_mux_flv_push(m, 2, asc, sizeof(asc), 0, 0, false) == RECORD_FLV_BUSY);
assert(be(p + 13, 3) == 0xffff9c);
assert(record_mux_flv_release(m));
assert(record_mux_flv_push(m, 1, predicted, sizeof(predicted), 89999, 90000, false) == RECORD_FLV_ERROR);
assert(record_mux_flv_push(m, 2, asc, sizeof(asc), 44100, 44100, false) == RECORD_FLV_PROGRESS);
assert(record_mux_flv_get(m, &s, &n) && s[1].data == asc);
p = (const unsigned char*)s[0].data;
assert(be(p + 4, 3) == 1000 && p[12] == 1);
assert(record_mux_flv_release(m));
assert(record_mux_flv_finish(m) && record_mux_flv_finish(m));
assert(record_mux_flv_step(m) == RECORD_FLV_END);
assert(record_mux_flv_push(m, 2, asc, sizeof(asc), 88200, 88200, false) == RECORD_FLV_ERROR);
record_mux_flv_free(m);
}
static void invalid(void)
{
record_flv_config_t c = config();
record_mux_flv_t *m;
unsigned char bad[sizeof(idr)];
unsigned i;
assert(!record_mux_flv_new(NULL));
for (i = 0; i < sizeof(avcc); i++)
{
c.avcc_size = i;
assert(!record_mux_flv_new(&c));
}
c = config();
c.max_packet_size = 0;
assert(!record_mux_flv_new(&c));
c = config();
c.video_timescale = 0;
assert(!record_mux_flv_new(&c));
for (i = 0; i < 3; i++)
{
c = config();
m = record_mux_flv_new(&c);
assert(m);
headers(m, 5);
memcpy(bad, idr, sizeof(bad));
if (i == 0)
bad[3] = 255;
if (i == 1)
bad[4] = 229;
if (i == 2)
bad[4] = 120;
assert(record_mux_flv_push(m, 1, bad, sizeof(bad), 0, 0, true) == RECORD_FLV_PROGRESS);
assert(record_mux_flv_step(m) == RECORD_FLV_ERROR);
assert(!record_mux_flv_finish(m));
record_mux_flv_free(m);
}
}
static void single_track(void)
{
record_flv_config_t c = config();
record_mux_flv_t *m;
c.avcc = NULL;
c.avcc_size = c.video_timescale = 0;
m = record_mux_flv_new(&c);
assert(m);
headers(m, 4);
assert(record_mux_flv_push(m, 1, idr, sizeof(idr), 0, 0, true) == RECORD_FLV_ERROR);
assert(record_mux_flv_push(m, 2, asc, sizeof(asc), 0, 1, false) == RECORD_FLV_ERROR);
assert(record_mux_flv_finish(m));
record_mux_flv_free(m);
c = config();
c.asc = NULL;
c.asc_size = c.audio_timescale = 0;
m = record_mux_flv_new(&c);
assert(m);
headers(m, 1);
assert(record_mux_flv_finish(m));
record_mux_flv_free(m);
}
static void timestamp_limits(void)
{
record_flv_config_t c = config();
record_mux_flv_t *m;
const record_flv_span_t *s;
const unsigned char *p;
size_t n;
c.video_timescale = 1000;
m = record_mux_flv_new(&c);
assert(m);
headers(m, 5);
assert(record_mux_flv_push(m, 1, idr, sizeof(idr), 0, 0x800000, true) == RECORD_FLV_ERROR);
assert(record_mux_flv_push(m, 1, idr, sizeof(idr), 0x800001, 0, true) == RECORD_FLV_ERROR);
assert(record_mux_flv_push(m, 1, idr, sizeof(idr), 0x800000, 0, true) == RECORD_FLV_PROGRESS);
assert(record_mux_flv_step(m) == RECORD_FLV_PROGRESS);
assert(record_mux_flv_step(m) == RECORD_FLV_PROGRESS);
assert(record_mux_flv_step(m) == RECORD_FLV_PACKET);
assert(record_mux_flv_get(m, &s, &n));
p = (const unsigned char*)s[0].data;
assert(be(p + 13, 3) == 0x800000);
assert(record_mux_flv_release(m));
assert(record_mux_flv_push(m, 1, predicted, sizeof(predicted),
0x7fffffff, 0x7fffffff, false) == RECORD_FLV_PROGRESS);
assert(record_mux_flv_step(m) == RECORD_FLV_PROGRESS);
assert(record_mux_flv_step(m) == RECORD_FLV_PACKET);
assert(record_mux_flv_get(m, &s, &n));
p = (const unsigned char*)s[0].data;
assert(p[7] == 127 && be(p + 4, 3) == 0xffffff);
assert(record_mux_flv_release(m));
assert(record_mux_flv_push(m, 1, predicted, sizeof(predicted),
(uint64_t)0x7fffffff + 1, (uint64_t)0x7fffffff + 1, false) == RECORD_FLV_ERROR);
assert(record_mux_flv_finish(m));
record_mux_flv_free(m);
}
int main(void)
{
lifetime();
invalid();
single_track();
timestamp_limits();
puts("PASS: FLV framing, borrowed packets, clocks, admission and teardown");
return 0;
}
+120
View File
@@ -0,0 +1,120 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "../../../record/record_mux_flv.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static unsigned read_be(const unsigned char *p, unsigned n)
{
unsigned v = 0, i;
for (i = 0; i < n; i++)
v = (v << 8) | p[i];
return v;
}
static void output(record_mux_flv_t *m, FILE *f)
{
const record_flv_span_t *s;
size_t n, i;
while (record_mux_flv_get(m, &s, &n))
{
for (i = 0; i < n; i++)
assert(fwrite(s[i].data, 1, s[i].size, f) == s[i].size);
assert(record_mux_flv_release(m));
}
}
int main(int argc, char **argv)
{
record_flv_config_t c;
record_mux_flv_t *m;
FILE *f;
unsigned char *data, *p;
size_t size, pos, length;
long bytes;
unsigned pass, type, timestamp, prefix;
int result, composition;
int64_t pts;
assert(argc == 3);
f = fopen(argv[1], "rb");
assert(f && !fseek(f, 0, SEEK_END));
bytes = ftell(f);
assert(bytes >= 13 && bytes < 64 * 1024 * 1024 && !fseek(f, 0, SEEK_SET));
size = (size_t)bytes;
data = (unsigned char*)malloc(size);
assert(data && fread(data, 1, size, f) == size && !fclose(f));
assert(!memcmp(data, "FLV\1", 4) && read_be(data + 5, 4) == 9);
memset(&c, 0, sizeof(c));
c.max_packet_size = 0xfffffa;
m = NULL;
f = NULL;
for (pass = 0; pass < 2; pass++)
{
for (pos = 13; pos < size; pos += length + 15)
{
assert(size - pos >= 15);
p = data + pos;
type = p[0];
length = read_be(p + 1, 3);
assert(length <= size - pos - 15);
assert(read_be(p + 11 + length, 4) == length + 11);
if (type != 8 && type != 9)
continue;
prefix = type == 9 ? 5 : 2;
assert(length >= prefix);
assert(type == 9 ? (p[11] & 15) == 7 : (p[11] >> 4) == 10);
if (type == 9 && p[12] == 2)
continue;
assert(length > prefix);
if (!pass)
{
if (p[12] != 0)
continue;
if (type == 9)
{
c.avcc = p + 16;
c.avcc_size = length - 5;
c.video_timescale = 1000;
}
else
{
c.asc = p + 13;
c.asc_size = length - 2;
c.audio_timescale = 1000;
}
continue;
}
if (p[12] != 1)
continue;
timestamp = read_be(p + 4, 3) | ((unsigned)p[7] << 24);
composition = type == 9 ? (int)read_be(p + 13, 3) : 0;
if (composition & 0x800000)
composition -= 0x1000000;
pts = (int64_t)timestamp + composition;
assert(pts >= 0);
result = record_mux_flv_push(m, type == 9 ? 1 : 2,
p + 11 + prefix, length - prefix, timestamp, (uint64_t)pts,
type == 9 && (p[11] >> 4) == 1);
assert(result == RECORD_FLV_PROGRESS);
do
result = record_mux_flv_step(m);
while (result == RECORD_FLV_PROGRESS);
assert(result == RECORD_FLV_PACKET);
output(m, f);
}
assert(pos == size);
if (!pass)
{
m = record_mux_flv_new(&c);
f = fopen(argv[2], "wb");
assert(m && f);
output(m, f);
}
}
assert(record_mux_flv_finish(m));
assert(!fclose(f));
record_mux_flv_free(m);
free(data);
return 0;
}
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Independent FLV packet, timestamp and decoded video/audio checks."""
import argparse
import json
from pathlib import Path
import shlex
import shutil
import subprocess
import tempfile
def command(args):
result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
timeout=60)
if result.returncode:
raise RuntimeError(result.stderr.decode(errors='replace'))
return result.stdout
def probe(path):
data = json.loads(command(['ffprobe', '-v', 'error', '-show_packets',
'-show_data_hash', 'sha256', '-of', 'json', str(path)]))
return [(p['codec_type'], p['dts'], p['pts'], p['size'], p['data_hash'])
for p in data['packets']]
def decode(path, audio):
return command(['ffmpeg', '-v', 'error', '-i', str(path), '-map',
'0:a:0' if audio else '0:v:0',
*(['-f', 'f32le'] if audio else
['-fps_mode', 'passthrough', '-pix_fmt', 'yuv420p', '-f', 'rawvideo']), '-'])
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--runner', default='')
parser.add_argument('binary')
args = parser.parse_args()
for tool in ('ffmpeg', 'ffprobe'):
if not shutil.which(tool):
raise SystemExit(f'{tool} is required (test only)')
binary = str(Path(args.binary).resolve())
with tempfile.TemporaryDirectory(prefix='flv-mux-') as temp:
root = Path(temp)
for bframes in (0, 2):
source, output = root / 'source.flv', root / 'output.flv'
command(['ffmpeg', '-v', 'error', '-y', '-f', 'lavfi', '-i',
'testsrc2=size=128x96:rate=25:duration=1', '-f', 'lavfi', '-i',
'sine=frequency=997:sample_rate=44100:duration=1',
'-c:v', 'libx264', '-threads', '1', '-bf', str(bframes),
'-g', '12', '-c:a', 'aac', '-profile:a', 'aac_low',
'-flvflags', 'no_duration_filesize', '-f', 'flv', str(source)])
command(shlex.split(args.runner) + [binary, str(source), str(output)])
assert probe(source) == probe(output), 'encoded packets or timestamps differ'
for audio in (False, True):
assert decode(source, audio) == decode(output, audio), 'decoded output differs'
print(f'PASS: independent FLV packet/timestamp/video/PCM oracle, B-frames={bframes}')
if __name__ == '__main__':
main()
+52
View File
@@ -0,0 +1,52 @@
CC ?= cc
RA := ../../..
CFLAGS += -O1 -g1 -std=c89 -pedantic -Wall -Wextra -Werror -Wframe-larger-than=2048 -I$(RA)/libretro-common/include
ifneq ($(SANITIZER),)
CFLAGS += -fsanitize=$(SANITIZER) -fno-omit-frame-pointer
LDFLAGS += -fsanitize=$(SANITIZER)
endif
all: rtmp_chunk_test$(EXE) rtmp_handshake_test$(EXE) rtmp_receive_test$(EXE) rtmp_command_test$(EXE) rtmp_reply_test$(EXE) rtmp_publish_test$(EXE) rtmp_control_test$(EXE) rtmp_session_test$(EXE)
SESSION := $(RA)/record/record_rtmp_session.c $(RA)/record/record_rtmp_handshake.c $(RA)/record/record_rtmp_chunk.c $(RA)/record/record_rtmp_receive.c $(RA)/record/record_rtmp_publish.c $(RA)/record/record_rtmp_command.c $(RA)/record/record_rtmp_reply.c $(RA)/record/record_rtmp_control.c
rtmp_session_test$(EXE): rtmp_session_test.c $(SESSION) $(wildcard $(RA)/record/record_rtmp*.h)
$(CC) $(CFLAGS) -o $@ rtmp_session_test.c $(SESSION) $(LDFLAGS)
rtmp_control_test$(EXE): rtmp_control_test.c $(RA)/record/record_rtmp_control.c $(RA)/record/record_rtmp_receive.c $(RA)/record/record_rtmp_control.h
$(CC) $(CFLAGS) -o $@ rtmp_control_test.c $(RA)/record/record_rtmp_control.c $(RA)/record/record_rtmp_receive.c $(LDFLAGS)
rtmp_chunk_test$(EXE): rtmp_chunk_test.c $(RA)/record/record_rtmp_chunk.c $(RA)/record/record_rtmp_chunk.h
$(CC) $(CFLAGS) -o $@ rtmp_chunk_test.c $(RA)/record/record_rtmp_chunk.c $(LDFLAGS)
rtmp_handshake_test$(EXE): rtmp_handshake_test.c $(RA)/record/record_rtmp_handshake.c $(RA)/record/record_rtmp_handshake.h $(RA)/record/record_rtmp_chunk.h
$(CC) $(CFLAGS) -o $@ rtmp_handshake_test.c $(RA)/record/record_rtmp_handshake.c $(LDFLAGS)
rtmp_receive_test$(EXE): rtmp_receive_test.c $(RA)/record/record_rtmp_receive.c $(RA)/record/record_rtmp_receive.h $(RA)/record/record_rtmp_chunk.h
$(CC) $(CFLAGS) -o $@ rtmp_receive_test.c $(RA)/record/record_rtmp_receive.c $(LDFLAGS)
rtmp_command_test$(EXE): rtmp_command_test.c $(RA)/record/record_rtmp_command.c $(RA)/record/record_rtmp_command.h $(RA)/record/record_rtmp_chunk.h
$(CC) $(CFLAGS) -o $@ rtmp_command_test.c $(RA)/record/record_rtmp_command.c $(LDFLAGS)
rtmp_reply_test$(EXE): rtmp_reply_test.c $(RA)/record/record_rtmp_reply.c $(RA)/record/record_rtmp_reply.h $(RA)/record/record_rtmp_chunk.h
$(CC) $(CFLAGS) -o $@ rtmp_reply_test.c $(RA)/record/record_rtmp_reply.c $(LDFLAGS)
PUBLISH := $(RA)/record/record_rtmp_publish.c $(RA)/record/record_rtmp_command.c $(RA)/record/record_rtmp_reply.c
PUBLISH_HEADERS := $(RA)/record/record_rtmp_publish.h $(RA)/record/record_rtmp_command.h $(RA)/record/record_rtmp_reply.h $(RA)/record/record_rtmp_receive.h $(RA)/record/record_rtmp_chunk.h
rtmp_publish_test$(EXE): rtmp_publish_test.c $(PUBLISH) $(PUBLISH_HEADERS)
$(CC) $(CFLAGS) -o $@ rtmp_publish_test.c $(PUBLISH) $(LDFLAGS)
check: all
$(RUNNER) ./rtmp_chunk_test$(EXE)
$(RUNNER) ./rtmp_handshake_test$(EXE)
$(RUNNER) ./rtmp_receive_test$(EXE)
$(RUNNER) ./rtmp_command_test$(EXE)
$(RUNNER) ./rtmp_reply_test$(EXE)
$(RUNNER) ./rtmp_publish_test$(EXE)
$(RUNNER) ./rtmp_control_test$(EXE)
$(RUNNER) ./rtmp_session_test$(EXE)
python3 run.py --runner "$(RUNNER)" ./rtmp_chunk_test$(EXE)
python3 receive.py --runner "$(RUNNER)" ./rtmp_receive_test$(EXE)
python3 command.py --runner "$(RUNNER)" ./rtmp_command_test$(EXE)
python3 reply.py --runner "$(RUNNER)" ./rtmp_reply_test$(EXE)
peer-check: all
python3 peer.py --runner "$(RUNNER)" --receive-binary ./rtmp_receive_test$(EXE) --command-binary ./rtmp_command_test$(EXE) --reply-binary ./rtmp_reply_test$(EXE) ./rtmp_handshake_test$(EXE)
python3 session_peer.py --runner "$(RUNNER)" ./rtmp_session_test$(EXE)
clean:
rm -f rtmp_chunk_test rtmp_chunk_test.exe rtmp_handshake_test rtmp_handshake_test.exe
rm -f rtmp_receive_test rtmp_receive_test.exe
rm -f rtmp_command_test rtmp_command_test.exe
rm -f rtmp_reply_test rtmp_reply_test.exe
rm -f rtmp_publish_test rtmp_publish_test.exe
rm -f rtmp_control_test rtmp_control_test.exe
rm -f rtmp_session_test rtmp_session_test.exe
.PHONY: all check peer-check clean
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Independent AMF0 decoding oracle for production publisher commands."""
import argparse
from pathlib import Path
import shlex
import struct
import subprocess
import tempfile
def decode(data):
pos = 0
def take(n):
nonlocal pos
assert n <= len(data) - pos
value = data[pos:pos + n]
pos += n
return value
def text():
return take(int.from_bytes(take(2), 'big')).decode('utf-8')
def value(depth=0):
assert depth < 8
marker = take(1)[0]
if marker == 0:
return struct.unpack('>d', take(8))[0]
if marker == 1:
flag = take(1)[0]
assert flag in (0, 1)
return bool(flag)
if marker == 2:
return text()
if marker == 5:
return None
assert marker == 3
result = {}
while data[pos:pos + 3] != b'\x00\x00\x09':
name = text()
assert name not in result
result[name] = value(depth + 1)
take(3)
return result
result = []
while pos < len(data):
result.append(value())
return result
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--runner', default='')
parser.add_argument('binary')
args = parser.parse_args()
expected = [
['connect', 1, {'app': 'live', 'tcUrl': 'rtmp://127.0.0.1/live',
'flashVer': 'FMLE/3.0 (compatible; RetroArch)', 'fpad': False,
'objectEncoding': 0}],
['createStream', 0xffffffff, None],
['publish', 0, None, 'test/é', 'live'],
['deleteStream', 0, None, 0xffffffff],
['releaseStream', 0x80000000, None, 'test/é'],
['FCPublish', 0x1000001, None, 'test/é']]
with tempfile.TemporaryDirectory(prefix='rtmp-amf-') as temp:
output = Path(temp) / 'commands.bin'
subprocess.run(shlex.split(args.runner) + [str(Path(args.binary).resolve()), str(output)],
check=True, timeout=30)
data = output.read_bytes()
pos = 0
for command in expected:
size = int.from_bytes(data[pos:pos + 2], 'big')
pos += 2
assert decode(data[pos:pos + size]) == command
pos += size
assert pos == len(data)
print('PASS: independent AMF0 publisher command and exact integer-number oracle')
if __name__ == '__main__':
main()
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""Test the production handshake against an independent loopback RTMP peer."""
import argparse
import os
from pathlib import Path
import shlex
import queue
import socket
import struct
import subprocess
import tempfile
import threading
import time
from command import decode as decode_amf
from reply import record as reply_record
def replies(read):
streams = {}
chunk_size = 128
records = bytearray()
for _ in range(1024):
first = read(1)[0]
fmt, ident = first >> 6, first & 63
if ident == 0:
ident = 64 + read(1)[0]
elif ident == 1:
ident = 64 + int.from_bytes(read(2), 'little')
previous = streams.get(ident)
if fmt == 0:
header = read(11)
stamp = int.from_bytes(header[:3], 'big')
current = {'length': int.from_bytes(header[3:6], 'big'),
'type': header[6], 'stream': int.from_bytes(header[7:], 'little'),
'extended': stamp == 0xffffff, 'data': bytearray()}
if current['extended']:
stamp = int.from_bytes(read(4), 'big')
current.update(time=stamp, delta=stamp, stamp=stamp)
else:
assert previous is not None, 'missing server chunk history'
current = previous
if fmt in (1, 2):
assert not current['data'], 'server changed header in partial message'
header = read(7 if fmt == 1 else 3)
stamp = int.from_bytes(header[:3], 'big')
current['extended'] = stamp == 0xffffff
if current['extended']:
stamp = int.from_bytes(read(4), 'big')
current.update(stamp=stamp, delta=stamp, time=(current['time'] + stamp) & 0xffffffff)
if fmt == 1:
current.update(length=int.from_bytes(header[3:6], 'big'), type=header[6])
else:
if current['extended']:
assert int.from_bytes(read(4), 'big') == current['stamp']
if not current['data']:
current['time'] = (current['time'] + current['delta']) & 0xffffffff
streams[ident] = current
assert current['length'] <= 8192 and len(streams) <= 16
current['data'].extend(read(min(chunk_size, current['length'] - len(current['data']))))
if len(current['data']) != current['length']:
continue
payload = bytes(current['data'])
records.extend(struct.pack('>IIIII', ident, current['stream'], current['type'],
current['time'], len(payload)) + payload)
current['data'].clear()
if current['type'] == 1:
assert len(payload) == 4
chunk_size = int.from_bytes(payload, 'big')
assert 0 < chunk_size <= 0x7fffffff
if current['type'] == 20 and payload.startswith(b'\x02\x00\x07_result'):
return records, payload
raise RuntimeError('no complete connect result within server chunk bound')
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--runner', default='')
parser.add_argument('--receive-binary', required=True)
parser.add_argument('--command-binary', required=True)
parser.add_argument('--reply-binary', required=True)
parser.add_argument('binary')
args = parser.parse_args()
ffmpeg = os.environ.get('FFMPEG', 'ffmpeg')
help_text = subprocess.run([ffmpeg, '-hide_banner', '-h', 'protocol=rtmp'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
check=True, timeout=10).stdout
if b'-rtmp_listen' not in help_text:
raise RuntimeError('FFMPEG must name a build with the native RTMP listener')
with socket.socket() as reserve:
reserve.bind(('127.0.0.1', 0))
port = reserve.getsockname()[1]
with tempfile.TemporaryFile() as log, tempfile.TemporaryFile() as errors:
server = subprocess.Popen([ffmpeg, '-v', 'error', '-listen', '1', '-timeout', '10', '-i',
f'rtmp://127.0.0.1:{port}/live/test', '-f', 'null', '-'],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=log)
client = None
try:
deadline = time.monotonic() + 10
while True:
try:
connection = socket.create_connection(('127.0.0.1', port), timeout=5)
break
except ConnectionRefusedError:
if server.poll() is not None or time.monotonic() >= deadline:
log.seek(0)
raise RuntimeError('RTMP listener failed: ' + log.read().decode(errors='replace'))
time.sleep(0.01)
client = subprocess.Popen(shlex.split(args.runner) +
[str(Path(args.binary).resolve()), '--peer'],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=errors)
with connection:
connection.settimeout(5)
def receive(size):
result = bytearray()
while len(result) < size:
part = connection.recv(size - len(result))
if not part:
raise RuntimeError('RTMP peer closed during handshake')
result.extend(part)
return result
def read_output(size):
result = queue.Queue()
reader = threading.Thread(target=lambda: result.put(client.stdout.read(size)),
daemon=True)
reader.start()
return result.get(timeout=10)
first = read_output(1537)
assert len(first) == 1537
connection.sendall(first)
client.stdin.write(receive(1537))
client.stdin.flush()
second = read_output(1536)
assert len(second) == 1536
connection.sendall(second)
client.stdin.write(receive(1536))
client.stdin.close()
result = client.wait(timeout=10)
if result:
errors.seek(0)
raise RuntimeError(errors.read().decode(errors='replace'))
with tempfile.TemporaryDirectory(prefix='rtmp-peer-command-') as temp:
output = Path(temp) / 'connect.bin'
subprocess.run(shlex.split(args.runner) +
[str(Path(args.command_binary).resolve()), '--connect', str(output),
f'rtmp://127.0.0.1:{port}/live'], check=True, timeout=30)
command = output.read_bytes()
header = b'\x03\x00\x00\x00' + len(command).to_bytes(3, 'big') + b'\x14\x00\x00\x00\x00'
connection.sendall(header + command[:128])
for offset in range(128, len(command), 128):
connection.sendall(b'\xc3' + command[offset:offset + 128])
reply = bytearray()
reply_deadline = time.monotonic() + 10
def read_reply(size):
remaining = reply_deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError('server connect result deadline expired')
connection.settimeout(min(5, remaining))
data = receive(size)
reply.extend(data)
assert len(reply) <= 131072, 'unbounded connect response'
return data
expected, command_reply = replies(read_reply)
with tempfile.TemporaryDirectory(prefix='rtmp-peer-reply-') as temp:
source, output = Path(temp) / 'input.bin', Path(temp) / 'output.bin'
source.write_bytes(reply)
subprocess.run(shlex.split(args.runner) +
[str(Path(args.receive_binary).resolve()), str(source), str(output)],
check=True, timeout=30)
assert output.read_bytes() == expected, 'production server-reply parsing differs'
source.write_bytes(b'\x14' + command_reply)
subprocess.run(shlex.split(args.runner) +
[str(Path(args.reply_binary).resolve()), str(source), str(output)],
check=True, timeout=30)
decoded = decode_amf(command_reply)
assert decoded[0] == '_result' and decoded[1] == 1
assert decoded[3]['code'] == 'NetConnection.Connect.Success'
normalized = reply_record(int(decoded[1]), None, decoded[0],
decoded[3].get('code', ''), decoded[3].get('level', ''),
decoded[3].get('description', ''))
assert output.read_bytes() == normalized, 'production AMF reply interpretation differs'
finally:
if client and client.poll() is None:
client.kill()
client.wait()
if server.poll() is None:
server.terminate()
try:
server.wait(timeout=10)
except subprocess.TimeoutExpired:
server.kill()
server.wait()
print('PASS: production RTMP handshake, connect and interpreted AMF reply with FFmpeg listener')
if __name__ == '__main__':
main()
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Independent compressed/interleaved RTMP receive fixtures."""
import argparse
from pathlib import Path
import shlex
import struct
import subprocess
import tempfile
def basic(fmt, ident):
if ident < 64:
return bytes([(fmt << 6) | ident])
if ident < 320:
return bytes([fmt << 6, ident - 64])
return bytes([(fmt << 6) | 1]) + (ident - 64).to_bytes(2, 'little')
def chunks(ident, stream, kind, stamp, payload, fmt=0, size=128):
header = basic(fmt, ident)
if fmt != 3:
header += min(stamp, 0xffffff).to_bytes(3, 'big')
if fmt <= 1:
header += len(payload).to_bytes(3, 'big') + bytes([kind])
if fmt == 0:
header += stream.to_bytes(4, 'little')
extended = stamp.to_bytes(4, 'big') if stamp >= 0xffffff else b''
result = [header + extended + payload[:size]]
for offset in range(size, len(payload), size):
result.append(basic(3, ident) + extended + payload[offset:offset + size])
return result
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--runner', default='')
parser.add_argument('binary')
args = parser.parse_args()
expected = bytearray()
wire = bytearray()
def expect(ident, stream, kind, timestamp, payload):
expected.extend(struct.pack('>IIIII', ident, stream, kind, timestamp, len(payload)))
expected.extend(payload)
video = bytes((i * 17 + 11) & 255 for i in range(300))
first = chunks(3, 1, 9, 1000, video)
control = struct.pack('>I', 65536)
wire.extend(first[0])
wire.extend(b''.join(chunks(2, 0, 5, 0, control)))
expect(2, 0, 5, 0, control)
wire.extend(b''.join(first[1:]))
expect(3, 1, 9, 1000, video)
cases = [(3, 1, 20, 10, 1010, 17, 1),
(3, 1, 20, 20, 1030, 17, 2), (3, 1, 20, 20, 1050, 17, 3),
(64, 9, 18, 0xfffffe, 0xfffffe, 129, 0),
(64, 9, 18, 0xfffffe, 0x1fffffc, 129, 3),
(319, 2, 20, 0xffffff, 0xffffff, 257, 0),
(319, 2, 20, 0x1000000, 0x1ffffff, 385, 1),
(319, 2, 20, 0x1000001, 0x3000000, 385, 2),
(319, 2, 20, 0x1000001, 0x4000001, 385, 3),
(65599, 4, 20, 0xfffffff0, 0xfffffff0, 2, 0),
(65599, 4, 20, 32, 16, 2, 2), (65599, 4, 20, 32, 48, 2, 3),
(320, 0, 4, 0, 0, 0, 0), (320, 0, 4, 0, 0, 0, 3)]
for index, (ident, stream, kind, stamp, timestamp, length, fmt) in enumerate(cases):
payload = bytes((i * 37 + index) & 255 for i in range(length))
wire.extend(b''.join(chunks(ident, stream, kind, stamp, payload, fmt)))
expect(ident, stream, kind, timestamp, payload)
control = struct.pack('>I', 7)
wire.extend(b''.join(chunks(2, 0, 1, 0, control)))
expect(2, 0, 1, 0, control)
payload = bytes((i * 19) & 255 for i in range(5000))
wire.extend(b''.join(chunks(3, 7, 18, 2222, payload, size=7)))
expect(3, 7, 18, 2222, payload)
with tempfile.TemporaryDirectory(prefix='rtmp-receive-') as temp:
source, output = Path(temp) / 'input.bin', Path(temp) / 'output.bin'
source.write_bytes(wire)
subprocess.run(shlex.split(args.runner) + [str(Path(args.binary).resolve()),
str(source), str(output)], check=True, timeout=30)
assert output.read_bytes() == expected, 'reassembled message or timestamp differs'
print('PASS: independent RTMP receive interleave, compression, extended time and rollover oracle')
if __name__ == '__main__':
main()
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Independent AMF reply fixtures and semantic extraction expectations."""
import argparse
from pathlib import Path
import shlex
import struct
import subprocess
import tempfile
def text(value):
data = value.encode('utf-8')
return len(data).to_bytes(2, 'big') + data
def amf(value):
if value is None:
return b'\x05'
if isinstance(value, bool):
return b'\x01' + bytes([255 if value else 0])
if isinstance(value, (int, float)):
return b'\x00' + struct.pack('>d', value)
if isinstance(value, str):
return b'\x02' + text(value)
if isinstance(value, dict):
return b'\x03' + b''.join(text(k) + amf(v) for k, v in value.items()) + b'\x00\x00\x09'
return b'\x0a' + len(value).to_bytes(4, 'big') + b''.join(amf(v) for v in value)
def record(transaction, stream, command, code='', level='', description=''):
output = b'\x01' + struct.pack('>II', transaction, stream or 0) + bytes([stream is not None])
for value in (command, code, level, description):
data = value.encode('utf-8')
output += len(data).to_bytes(4, 'big') + data
return output
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--runner', default='')
parser.add_argument('binary')
args = parser.parse_args()
cases = []
prefix = amf('_result') + amf(1) + amf({'capabilities': 31, 'fmsVer': 'test'})
info = {'level': 'status', 'code': 'NetConnection.Connect.Success', 'description': 'prêt'}
cases.append((b'\x14' + prefix + amf(info),
record(1, None, '_result', info['code'], info['level'], info['description'])))
for number in (0, 1, 2, 0x1000001, 0x80000000, 0xffffffff):
data = b''.join(amf(v) for v in ('_result', number, None, number))
cases.append((b'\x14' + data, record(number, number, '_result')))
cases.append((b'\x11\x00' + data, record(number, number, '_result')))
status = {'nested': {'code': 'spoof'}, 'extra': [True, None, {'level': 'wrong'}],
'code': 'NetStream.Publish.Start', 'level': 'status'}
cases.append((b'\x14' + b''.join(amf(v) for v in ('onStatus', 0, None, status)),
record(0, None, 'onStatus', status['code'], status['level'])))
error = {'code': 'NetStream.Publish.BadName', 'level': 'error'}
cases.append((b'\x14' + b''.join(amf(v) for v in ('_error', 2, None, error)),
record(2, None, '_error', error['code'], error['level'])))
cases.append((b'\x14' + amf('onFCPublish'), record(0, None, 'onFCPublish')))
cases.append((b'\x11\x00' + amf('onFCPublish'), record(0, None, 'onFCPublish')))
for name in ('_result', '_error', 'onStatus'):
cases.append((b'\x14' + amf(name), b'\x00'))
for number in (-1, 0.5, float('inf'), float('nan'), 0x100000000):
cases.append((b'\x14' + amf('_result') + amf(number) + amf(None), b'\x00'))
cases.append((b'\x14' + amf('_result') + amf(2) + amf(None) + amf(number), b'\x00'))
duplicate = b'\x03' + text('code') + amf('first') + text('code') + amf('second') + b'\x00\x00\x09'
cases.append((b'\x14' + prefix + duplicate, b'\x00'))
deep = None
for _ in range(9):
deep = {'child': deep}
cases.append((b'\x14' + prefix + amf(deep), b'\x00'))
cases.append((b'\x14' + prefix + b'\x11', b'\x00'))
status_payload = amf(info)
status_expected = record(1, None, '_result', info['code'], info['level'], info['description'])
cases.append((b'\x14' + prefix + b'\x08\xff\xff\xff\xff' + status_payload[1:], status_expected))
cases.append((b'\x14' + prefix + b'\x10' + text('Status') + status_payload[1:], status_expected))
extras = (text('reference') + b'\x07\x00\x00' + text('date') + b'\x0b' + bytes(10) +
text('xml') + b'\x0f\x00\x00\x00\x04<a/>' + text('undefined') + b'\x06')
cases.append((b'\x14' + prefix + b'\x03' + extras + status_payload[1:], status_expected))
cases.append((b'\x14' + prefix + b'\x03' + text('ref') + b'\x07\xff\xff\x00\x00\x09', b'\x00'))
cases.append((b'\x14' + prefix + b'\x0a\xff\xff\xff\xff', b'\x00'))
cases.append((b'\x14' + prefix + amf({'code': None}), b'\x00'))
with tempfile.TemporaryDirectory(prefix='rtmp-reply-') as temp:
source, output = Path(temp) / 'input.bin', Path(temp) / 'output.bin'
for index, (wire, expected) in enumerate(cases):
source.write_bytes(wire)
subprocess.run(shlex.split(args.runner) + [str(Path(args.binary).resolve()),
str(source), str(output)], check=True, timeout=30)
assert output.read_bytes() == expected, f'AMF reply fixture {index} differs'
print(f'PASS: {len(cases)} independent AMF reply ID/status/error/depth fixtures')
if __name__ == '__main__':
main()
+107
View File
@@ -0,0 +1,107 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "../../../record/record_rtmp_chunk.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
static unsigned char payload[700];
static void fixtures(FILE *file, unsigned chunk_size)
{
static const unsigned ids[] = { 2, 63, 64, 319, 320, 65599 };
static const uint32_t times[] = { 0, 0xfffffe, 0xffffff, 0x1000000, 0xffffffff, 42 };
static const size_t sizes[] = { 0, 1, 127, 128, 129, 700 };
record_rtmp_chunk_t *c = record_rtmp_chunk_new(chunk_size);
record_rtmp_span_t in[3], out, again;
size_t i, n, step = 0, sent;
bool borrowed;
assert(c);
for (i = 0; i < 6; i++)
{
in[0].data = payload;
in[0].size = sizes[i];
n = sizes[i] ? 1 : 0;
if (sizes[i] == 700)
{
in[0].size = 17;
in[1].data = payload + 17;
in[1].size = 111;
in[2].data = payload + 128;
in[2].size = 572;
n = 3;
}
assert(record_rtmp_chunk_push(c, ids[i], 0x12345678, 9, times[i], in, n));
assert(!record_rtmp_chunk_size(c, chunk_size));
assert(!record_rtmp_chunk_push(c, 2, 1, 8, 0, NULL, 0));
sent = 0;
while (record_rtmp_chunk_get(c, &out))
{
assert(out.size);
assert(!record_rtmp_chunk_consume(c, out.size + 1));
assert(record_rtmp_chunk_consume(c, 0));
assert(record_rtmp_chunk_get(c, &again));
assert(out.data == again.data && out.size == again.size);
borrowed = false;
for (n = 0; n < sizes[i]; n++)
if (out.data == payload + n)
{
assert(n == sent);
borrowed = true;
break;
}
n = 1 + step++ % 19;
if (n > out.size)
n = out.size;
if (borrowed)
sent += n;
if (file)
assert(fwrite(out.data, 1, n, file) == n);
assert(record_rtmp_chunk_consume(c, n));
}
assert(sent == sizes[i]);
assert(record_rtmp_chunk_size(c, chunk_size));
}
assert(!record_rtmp_chunk_consume(c, 0));
record_rtmp_chunk_free(c);
}
static void invalid(void)
{
record_rtmp_chunk_t *c = record_rtmp_chunk_new(128);
record_rtmp_span_t s;
assert(c && !record_rtmp_chunk_new(0));
assert(!record_rtmp_chunk_size(c, 0));
s.data = payload;
s.size = 0x1000000;
assert(!record_rtmp_chunk_push(c, 2, 1, 9, 0, &s, 1));
s.size = 1;
assert(!record_rtmp_chunk_push(c, 1, 1, 9, 0, &s, 1));
assert(!record_rtmp_chunk_push(c, 65600, 1, 9, 0, &s, 1));
assert(!record_rtmp_chunk_push(c, 2, 1, 256, 0, &s, 1));
assert(!record_rtmp_chunk_push(c, 2, 1, 9, 0, &s, 4));
assert(!record_rtmp_chunk_push(c, 2, 1, 9, 0, NULL, 1));
assert(record_rtmp_chunk_push(c, 2, 1, 9, 0, &s, 1));
record_rtmp_chunk_free(c);
}
int main(int argc, char **argv)
{
unsigned i;
FILE *file = NULL;
assert(argc == 1 || argc == 2);
for (i = 0; i < sizeof(payload); i++)
payload[i] = (unsigned char)(i * 37 + 11);
if (argc == 2)
{
file = fopen(argv[1], "wb");
assert(file);
}
fixtures(file, 128);
if (file)
assert(!fclose(file));
fixtures(NULL, 1);
fixtures(NULL, 4096);
invalid();
puts("PASS: RTMP chunk ownership, short writes, extended timestamps and IDs");
return 0;
}
@@ -0,0 +1,100 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "../../../record/record_rtmp_command.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
static uint8_t golden[1024];
static char oversized[4098];
static record_rtmp_command_config_t config(unsigned command)
{
record_rtmp_command_config_t c;
c.application = "live";
c.url = "rtmp://127.0.0.1/live";
c.name = "test/\303\251";
c.transaction = command == RECORD_RTMP_RELEASE_STREAM ? 0x80000000
: command == RECORD_RTMP_FC_PUBLISH ? 0x1000001 : 0xffffffff;
c.stream = 0xffffffff;
return c;
}
static void bounds(void)
{
uint8_t output[1026];
record_rtmp_command_config_t c;
size_t size, needed, cap, i;
unsigned command;
for (command = RECORD_RTMP_CONNECT; command <= RECORD_RTMP_FC_PUBLISH; command++)
{
c = config(command);
assert(record_rtmp_command_encode((enum record_rtmp_command)command,
&c, golden, sizeof(golden), &needed));
for (cap = 0; cap < needed; cap++)
{
memset(output, 0xa5, sizeof(output));
size = 999;
assert(!record_rtmp_command_encode((enum record_rtmp_command)command,
&c, output, cap, &size) && !size);
for (i = cap; i < sizeof(output); i++)
assert(output[i] == 0xa5);
}
assert(record_rtmp_command_encode((enum record_rtmp_command)command,
&c, output, needed, &size) && size == needed);
assert(!memcmp(output, golden, needed));
}
memset(oversized, 'a', sizeof(oversized) - 1);
c = config(0);
c.application = oversized;
assert(!record_rtmp_command_encode(RECORD_RTMP_CONNECT, &c, output, sizeof(output), &size) && !size);
c = config(0);
c.url = NULL;
assert(!record_rtmp_command_encode(RECORD_RTMP_CONNECT, &c, output, sizeof(output), &size));
c = config(1);
c.transaction = 0;
assert(!record_rtmp_command_encode(RECORD_RTMP_CREATE_STREAM, &c, output, sizeof(output), &size));
c = config(2);
c.name = "";
assert(!record_rtmp_command_encode(RECORD_RTMP_PUBLISH, &c, output, sizeof(output), &size));
c = config(3);
c.stream = 0;
assert(!record_rtmp_command_encode(RECORD_RTMP_DELETE_STREAM, &c, output, sizeof(output), &size));
assert(!record_rtmp_command_encode((enum record_rtmp_command)99, &c, output, sizeof(output), &size));
}
int main(int argc, char **argv)
{
record_rtmp_command_config_t c;
FILE *f;
size_t size;
unsigned command;
uint8_t length[2];
if (argc == 4 && !strcmp(argv[1], "--connect"))
{
c = config(RECORD_RTMP_CONNECT);
c.url = argv[3];
assert(record_rtmp_command_encode(RECORD_RTMP_CONNECT, &c, golden, sizeof(golden), &size));
f = fopen(argv[2], "wb");
assert(f && fwrite(golden, 1, size, f) == size && !fclose(f));
return 0;
}
assert(argc == 1 || argc == 2);
bounds();
if (argc == 2)
{
f = fopen(argv[1], "wb");
assert(f);
for (command = 0; command <= RECORD_RTMP_FC_PUBLISH; command++)
{
c = config(command);
assert(record_rtmp_command_encode((enum record_rtmp_command)command,
&c, golden, sizeof(golden), &size));
length[0] = (uint8_t)(size >> 8);
length[1] = (uint8_t)size;
assert(fwrite(length, 1, 2, f) == 2 && fwrite(golden, 1, size, f) == size);
}
assert(!fclose(f));
}
puts("PASS: AMF command admission, exact capacity and output bounds");
return 0;
}
@@ -0,0 +1,218 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "../../../record/record_rtmp_control.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
static int send_control(record_rtmp_control_t *c, record_rtmp_receive_t *r,
unsigned type, const uint8_t *bytes, size_t size)
{
record_rtmp_message_t m;
m.payload.data = bytes;
m.payload.size = size;
m.timestamp = 0;
m.stream_id = 0;
m.chunk_id = 2;
m.type = type;
return record_rtmp_control_handle(c, r, &m);
}
static void response(record_rtmp_control_t *c, unsigned type,
const uint8_t *bytes, size_t size)
{
record_rtmp_message_t m, again;
assert(record_rtmp_control_get(c, &m));
assert(m.chunk_id == 2 && !m.stream_id && !m.timestamp);
assert(m.type == type && m.payload.size == size);
assert(!memcmp(m.payload.data, bytes, size));
assert(record_rtmp_control_get(c, &again));
assert(m.payload.data == again.payload.data);
assert(!record_rtmp_control_free(c));
}
static void accounting(void)
{
record_rtmp_control_t *c = record_rtmp_control_new();
record_rtmp_message_t m;
record_rtmp_control_status_t s;
const uint8_t window[] = {0, 0, 0, 4};
const uint8_t four[] = {0, 0, 0, 4};
const uint8_t eight[] = {0, 0, 0, 8};
const uint8_t wrapped[] = {0, 0, 0, 7};
assert(c);
assert(send_control(c, NULL, 5, window, 4) == RECORD_RTMP_CONTROL_HANDLED);
assert(record_rtmp_control_received(c, 3));
assert(!record_rtmp_control_get(c, &m));
assert(record_rtmp_control_received(c, 1));
response(c, 3, four, 4);
assert(record_rtmp_control_received(c, 4));
response(c, 3, four, 4);
assert(record_rtmp_control_ack(c));
response(c, 3, eight, 4);
assert(record_rtmp_control_ack(c));
assert(record_rtmp_control_received(c, (size_t)0xffffffffu));
response(c, 3, wrapped, 4);
assert(record_rtmp_control_ack(c));
assert(!record_rtmp_control_get(c, &m));
assert(send_control(c, NULL, 3, eight, 4) == RECORD_RTMP_CONTROL_HANDLED);
assert(record_rtmp_control_status(c, &s));
assert(s.peer_ack == 8 && s.window == 4);
assert(s.received == (uint64_t)0xffffffffu + 8);
assert(record_rtmp_control_free(c));
}
static void ping_and_bandwidth(void)
{
record_rtmp_control_t *c = record_rtmp_control_new();
record_rtmp_control_status_t s;
record_rtmp_message_t m;
uint8_t ping[] = {0, 6, 0xde, 0xad, 0xbe, 0xef};
const uint8_t pong[] = {0, 7, 0xde, 0xad, 0xbe, 0xef};
uint8_t bandwidth[] = {0, 0, 4, 0, 2};
const uint8_t smaller[] = {0, 0, 2, 0};
assert(c);
assert(send_control(c, NULL, 6, bandwidth, 5) == RECORD_RTMP_CONTROL_HANDLED);
assert(!record_rtmp_control_get(c, &m));
bandwidth[4] = 0;
assert(send_control(c, NULL, 6, bandwidth, 5) == RECORD_RTMP_CONTROL_HANDLED);
response(c, 5, bandwidth, 4);
bandwidth[2] = 2;
bandwidth[4] = 2;
assert(send_control(c, NULL, 6, bandwidth, 5) == RECORD_RTMP_CONTROL_BUSY);
assert(record_rtmp_control_ack(c));
assert(send_control(c, NULL, 6, bandwidth, 5) == RECORD_RTMP_CONTROL_HANDLED);
response(c, 5, smaller, 4);
assert(send_control(c, NULL, 4, ping, 6) == RECORD_RTMP_CONTROL_HANDLED);
memset(ping, 0, sizeof(ping));
response(c, 5, smaller, 4);
assert(record_rtmp_control_ack(c));
response(c, 4, pong, 6);
ping[1] = 6;
assert(send_control(c, NULL, 4, ping, 6) == RECORD_RTMP_CONTROL_BUSY);
assert(record_rtmp_control_ack(c));
bandwidth[2] = 4;
bandwidth[4] = 1;
assert(send_control(c, NULL, 6, bandwidth, 5) == RECORD_RTMP_CONTROL_HANDLED);
assert(record_rtmp_control_status(c, &s) && s.bandwidth == 512);
assert(!record_rtmp_control_get(c, &m));
bandwidth[2] = 1;
bandwidth[4] = 2;
assert(send_control(c, NULL, 6, bandwidth, 5) == RECORD_RTMP_CONTROL_HANDLED);
assert(record_rtmp_control_status(c, &s) && s.bandwidth == 512);
assert(record_rtmp_control_free(c));
}
static void receive_controls(void)
{
static const uint8_t wire[] =
{
2, 0, 0, 0, 0, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 1,
3, 0, 0, 0, 0, 0, 3, 20, 1, 0, 0, 0, 'A', 0xc3, 'B', 0xc3, 'C',
3, 0, 0, 0, 0, 0, 3, 20, 1, 0, 0, 0, 'D',
2, 0, 0, 0, 0, 0, 4, 2, 0, 0, 0, 0, 0, 0xc2, 0, 0xc2, 0, 0xc2, 3,
3, 0, 0, 0, 0, 0, 3, 20, 1, 0, 0, 0, 'E', 0xc3, 'F', 0xc3, 'G'
};
record_rtmp_control_t *c = record_rtmp_control_new();
record_rtmp_receive_t *r = record_rtmp_receive_new(64, 4);
record_rtmp_message_t m;
size_t at = 0, used;
unsigned messages = 0;
int result;
const uint8_t abort[] = {0, 0, 0, 64};
assert(c && r);
while (at < sizeof(wire))
{
result = record_rtmp_receive_feed(r, wire + at, 1, &used);
assert(result != RECORD_RTMP_RECEIVE_ERROR && used == 1);
at += used;
assert(record_rtmp_control_received(c, used));
if (result != RECORD_RTMP_RECEIVE_MESSAGE)
continue;
assert(record_rtmp_receive_get(r, &m));
result = record_rtmp_control_handle(c, r, &m);
if (m.type != 20)
assert(result == RECORD_RTMP_CONTROL_HANDLED);
else
{
assert(result == RECORD_RTMP_CONTROL_OTHER);
assert(m.payload.size == 3);
assert(!memcmp(m.payload.data, messages == 1 ? "ABC" : "EFG", 3));
}
messages++;
assert(record_rtmp_receive_release(r));
}
assert(messages == 4);
assert(send_control(c, r, 2, abort, 4) == RECORD_RTMP_CONTROL_HANDLED);
assert(record_rtmp_receive_finish(r));
record_rtmp_receive_free(r);
assert(record_rtmp_control_free(c));
}
static void malformed(void)
{
unsigned type, size;
const uint8_t bytes[] = {0, 0, 0, 1, 0};
for (type = 1; type <= 6; type++)
for (size = 0; size < 6; size++)
{
record_rtmp_control_t *c;
record_rtmp_receive_t *r;
record_rtmp_message_t m;
if (type == 4 || size == (type == 6 ? 5 : 4))
continue;
c = record_rtmp_control_new();
r = record_rtmp_receive_new(64, 4);
assert(c && r);
assert(send_control(c, r, type, bytes, size) == RECORD_RTMP_CONTROL_ERROR);
assert(!record_rtmp_control_received(c, 1));
assert(!record_rtmp_control_get(c, &m));
assert(record_rtmp_control_free(c));
record_rtmp_receive_free(r);
}
}
static void invalid_values_and_retirement(void)
{
static const unsigned types[] = {1, 1, 2, 5, 6, 4, 4};
static const size_t sizes[] = {4, 4, 4, 4, 5, 2, 5};
static const uint8_t values[][6] =
{
{0, 0, 0, 0}, {128, 0, 0, 1}, {0, 0, 0, 0},
{0, 0, 0, 0}, {0, 0, 0, 1, 3}, {0, 6}, {0, 7, 0, 0, 0}
};
unsigned i;
for (i = 0; i < sizeof(types) / sizeof(types[0]); i++)
{
record_rtmp_control_t *c = record_rtmp_control_new();
record_rtmp_receive_t *r = record_rtmp_receive_new(64, 4);
record_rtmp_message_t m;
const uint8_t ping[] = {0, 6, 1, 2, 3, 4};
assert(c && r);
assert(send_control(c, r, 4, ping, 6) == RECORD_RTMP_CONTROL_HANDLED);
assert(record_rtmp_control_get(c, &m));
assert(send_control(c, r, types[i], values[i], sizes[i]) == RECORD_RTMP_CONTROL_ERROR);
assert(!record_rtmp_control_free(c));
assert(!memcmp((const uint8_t*)m.payload.data + 2, ping + 2, 4));
assert(record_rtmp_control_ack(c));
assert(record_rtmp_control_free(c));
record_rtmp_receive_free(r);
}
if (sizeof(size_t) >= sizeof(uint64_t))
{
record_rtmp_control_t *c = record_rtmp_control_new();
assert(c && record_rtmp_control_received(c, (size_t)-1));
assert(!record_rtmp_control_received(c, 1));
assert(record_rtmp_control_free(c));
}
}
int main(void)
{
accounting();
ping_and_bandwidth();
receive_controls();
malformed();
invalid_values_and_retirement();
puts("RTMP controls: acknowledgements, ping, bandwidth, chunk resize and malformed input pass");
return 0;
}
@@ -0,0 +1,175 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "../../../record/record_rtmp_handshake.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
#ifdef _WIN32
#include <fcntl.h>
#include <io.h>
#endif
static uint8_t random_bytes[1528], server[3076], client[3073];
static record_rtmp_handshake_t *setup(void)
{
record_rtmp_handshake_t *h;
record_rtmp_span_t s;
unsigned i;
for (i = 0; i < 1528; i++)
random_bytes[i] = (uint8_t)(i * 13 + 7);
h = record_rtmp_handshake_new(random_bytes, 0x12345678);
assert(h && record_rtmp_handshake_get(h, &s));
assert(s.size == 1537);
memcpy(client, s.data, s.size);
assert(client[0] == 3 && client[1] == 18 && client[4] == 120);
assert(!memcmp(client + 5, "\0\0\0\0", 4));
assert(!memcmp(client + 9, random_bytes, 1528));
memset(random_bytes, 0, sizeof(random_bytes));
memset(server, 0, sizeof(server));
server[0] = 3;
server[1] = 1;
server[4] = 2;
for (i = 9; i < 1537; i++)
server[i] = (uint8_t)(i * 17 + 3);
memcpy(server + 1537, client + 1, 1536);
memset(server + 1541, 99, 4);
server[3073] = 44;
return h;
}
static void send_output(record_rtmp_handshake_t *h, size_t *sent)
{
record_rtmp_span_t s, again;
size_t n;
while (record_rtmp_handshake_get(h, &s))
{
assert(!record_rtmp_handshake_consume(h, s.size + 1));
assert(record_rtmp_handshake_consume(h, 0));
assert(record_rtmp_handshake_get(h, &again));
assert(again.data == s.data && again.size == s.size);
n = s.size < 23 ? s.size : 23;
assert(*sent + n <= sizeof(client));
memcpy(client + *sent, s.data, n);
assert(record_rtmp_handshake_consume(h, n));
*sent += n;
}
}
static void ordering(bool receive_first, bool fragmented)
{
record_rtmp_handshake_t *h = setup();
size_t sent = 0, pos = 0, n, used;
int result;
if (!receive_first)
{
send_output(h, &sent);
assert(sent == 1537);
}
while (pos < 3073)
{
n = fragmented ? 1 + pos % 37 : sizeof(server) - pos;
if (n > sizeof(server) - pos)
n = sizeof(server) - pos;
result = record_rtmp_handshake_feed(h, server + pos, n, &used, 0x87654321);
assert(result != RECORD_RTMP_HANDSHAKE_ERROR && used && used <= n);
pos += used;
if (!receive_first)
send_output(h, &sent);
}
assert(pos == 3073 && server[pos] == 44);
if (receive_first)
{
assert(record_rtmp_handshake_status(h) == RECORD_RTMP_HANDSHAKE_PENDING);
send_output(h, &sent);
}
assert(sent == sizeof(client));
assert(!memcmp(client + 1537, server + 1, 4));
assert(!memcmp(client + 1541, "\207\145\103\41", 4));
assert(!memcmp(client + 1545, server + 9, 1528));
assert(record_rtmp_handshake_status(h) == RECORD_RTMP_HANDSHAKE_DONE);
assert(record_rtmp_handshake_feed(h, server + pos, 3, &used, 0) == RECORD_RTMP_HANDSHAKE_DONE);
assert(used == 0);
record_rtmp_handshake_free(h);
}
static void corrupt(unsigned offset)
{
record_rtmp_handshake_t *h = setup();
record_rtmp_span_t s;
size_t pos = 0, used;
int result = 0;
server[offset] ^= 1;
while (pos < 3073 && result != RECORD_RTMP_HANDSHAKE_ERROR)
{
result = record_rtmp_handshake_feed(h, server + pos, 3073 - pos, &used, 0);
assert(used);
pos += used;
}
assert(result == RECORD_RTMP_HANDSHAKE_ERROR);
assert(record_rtmp_handshake_status(h) == RECORD_RTMP_HANDSHAKE_ERROR);
assert(!record_rtmp_handshake_get(h, &s));
assert(record_rtmp_handshake_feed(h, NULL, 0, &used, 0) == RECORD_RTMP_HANDSHAKE_ERROR);
assert(!used);
record_rtmp_handshake_free(h);
}
static void peer_output(record_rtmp_handshake_t *h)
{
record_rtmp_span_t s;
while (record_rtmp_handshake_get(h, &s))
{
assert(fwrite(s.data, 1, s.size, stdout) == s.size);
assert(record_rtmp_handshake_consume(h, s.size));
}
assert(!fflush(stdout));
}
static int peer(void)
{
record_rtmp_handshake_t *h;
size_t i, pos, used, size;
unsigned part;
#ifdef _WIN32
assert(_setmode(_fileno(stdin), _O_BINARY) != -1);
assert(_setmode(_fileno(stdout), _O_BINARY) != -1);
#endif
for (i = 0; i < sizeof(random_bytes); i++)
random_bytes[i] = (uint8_t)(i * 13 + 7);
h = record_rtmp_handshake_new(random_bytes, 0);
assert(h);
peer_output(h);
for (part = 0; part < 2; part++)
{
size = part ? 1536 : 1537;
assert(fread(server, 1, size, stdin) == size);
for (pos = 0; pos < size; pos += used)
{
assert(record_rtmp_handshake_feed(h, server + pos, size - pos,
&used, 0) != RECORD_RTMP_HANDSHAKE_ERROR);
assert(used);
}
peer_output(h);
}
assert(record_rtmp_handshake_status(h) == RECORD_RTMP_HANDSHAKE_DONE);
record_rtmp_handshake_free(h);
return 0;
}
int main(int argc, char **argv)
{
if (argc == 2 && !strcmp(argv[1], "--peer"))
return peer();
assert(argc == 1);
assert(!record_rtmp_handshake_new(NULL, 0));
ordering(false, false);
ordering(false, true);
ordering(true, false);
ordering(true, true);
corrupt(0);
corrupt(5);
corrupt(1537);
corrupt(1545);
corrupt(3072);
puts("PASS: fragmented RTMP handshake, output ordering, echo and trailing bytes");
return 0;
}
@@ -0,0 +1,181 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "../../../record/record_rtmp_publish.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
static const uint8_t connected[] =
"\2\0\7_result\0\77\360\0\0\0\0\0\0\5\3"
"\0\4code\2\0\35NetConnection.Connect.Success\0\0\11";
static const uint8_t created[] =
{ 2, 0, 7, '_', 'r', 'e', 's', 'u', 'l', 't',
0, 64, 0, 0, 0, 0, 0, 0, 0, 5, 0, 64, 49, 0, 0, 0, 0, 0, 0 };
static const uint8_t started[] =
"\2\0\10onStatus\0\0\0\0\0\0\0\0\0\5\3"
"\0\4code\2\0\27NetStream.Publish.Start\0\0\11";
static const uint8_t rejected[] =
"\2\0\10onStatus\0\0\0\0\0\0\0\0\0\5\3"
"\0\5level\2\0\5error\0\0\11";
static const uint8_t connection_error[] = "\2\0\6_error\0\77\360\0\0\0\0\0\0\5";
static record_rtmp_publish_t *make_publish(bool legacy)
{
record_rtmp_publish_config_t c;
char app[] = "live", url[] = "rtmp://localhost/live", name[] = "test";
record_rtmp_publish_t *p;
c.application = app;
c.url = url;
c.name = name;
c.legacy_commands = legacy;
p = record_rtmp_publish_new(&c);
assert(p);
memset(app, 0, sizeof(app));
memset(url, 0, sizeof(url));
memset(name, 0, sizeof(name));
return p;
}
static void output(record_rtmp_publish_t *p, const char *command, uint32_t stream)
{
record_rtmp_message_t message, again;
const uint8_t *bytes;
size_t size = strlen(command);
assert(record_rtmp_publish_get(p, &message));
assert(record_rtmp_publish_get(p, &again));
assert(message.payload.data == again.payload.data && message.payload.size == again.payload.size);
assert(message.stream_id == stream && message.type == 20 && message.timestamp == 0 && message.chunk_id == 3);
bytes = (const uint8_t*)message.payload.data;
assert(bytes[0] == 2 && bytes[1] == 0 && bytes[2] == size);
assert(!memcmp(bytes + 3, command, size));
if (!strcmp(command, "publish"))
assert(message.payload.size >= 14
&& !memcmp(bytes + message.payload.size - 14, "\2\0\4test\2\0\4live", 14));
if (!strcmp(command, "releaseStream") || !strcmp(command, "FCPublish"))
assert(message.payload.size >= 7
&& !memcmp(bytes + message.payload.size - 7, "\2\0\4test", 7));
assert(!record_rtmp_publish_free(p));
assert(record_rtmp_publish_ack(p));
}
static void input(record_rtmp_publish_t *p, const void *data, size_t size, uint32_t stream)
{
record_rtmp_message_t m;
unsigned i;
int result = RECORD_RTMP_PUBLISH_PROGRESS;
m.payload.data = data;
m.payload.size = size;
m.stream_id = stream;
m.type = 20;
m.chunk_id = 3;
m.timestamp = 0;
assert(record_rtmp_publish_reply(p, &m));
assert(!record_rtmp_publish_reply(p, &m));
assert(!record_rtmp_publish_free(p));
for (i = 0; i < 512 && result == RECORD_RTMP_PUBLISH_PROGRESS; i++)
result = record_rtmp_publish_step(p);
assert(result == RECORD_RTMP_PUBLISH_RELEASE_REPLY);
}
static void lifecycle(bool legacy)
{
record_rtmp_publish_t *p = make_publish(legacy);
record_rtmp_message_t m;
enum record_rtmp_publish_error error;
uint32_t stream;
uint8_t reply[sizeof(created)];
assert(record_rtmp_publish_step(p) == RECORD_RTMP_PUBLISH_BUSY);
output(p, "connect", 0);
input(p, connected, sizeof(connected) - 1, 0);
if (legacy)
{
output(p, "releaseStream", 0);
output(p, "FCPublish", 0);
}
output(p, "createStream", 0);
input(p, connected, sizeof(connected) - 1, 0);
assert(!record_rtmp_publish_get(p, &m));
memcpy(reply, created, sizeof(reply));
if (legacy)
reply[12] = 16;
input(p, reply, sizeof(reply), 0);
output(p, "publish", 17);
input(p, started, sizeof(started) - 1, 18);
assert(record_rtmp_publish_status(p, &stream, &error) == RECORD_RTMP_PUBLISH_NEGOTIATING);
input(p, started, sizeof(started) - 1, 17);
assert(record_rtmp_publish_step(p) == RECORD_RTMP_PUBLISH_ESTABLISHED);
assert(record_rtmp_publish_status(p, &stream, &error) == RECORD_RTMP_PUBLISH_READY);
assert(stream == 17 && error == RECORD_RTMP_PUBLISH_ERROR_NONE);
input(p, rejected, sizeof(rejected) - 1, 18);
assert(record_rtmp_publish_step(p) == RECORD_RTMP_PUBLISH_ESTABLISHED);
input(p, rejected, sizeof(rejected) - 1, 17);
record_rtmp_publish_cancel(p);
assert(record_rtmp_publish_step(p) == RECORD_RTMP_PUBLISH_ERROR);
assert(record_rtmp_publish_status(p, &stream, &error) == RECORD_RTMP_PUBLISH_FAILED);
assert(error == RECORD_RTMP_PUBLISH_ERROR_REJECTED);
assert(record_rtmp_publish_free(p));
}
static void cancellation(void)
{
record_rtmp_publish_t *p = make_publish(false);
record_rtmp_message_t m, again;
uint8_t saved[512];
assert(record_rtmp_publish_get(p, &m) && m.payload.size < sizeof(saved));
memcpy(saved, m.payload.data, m.payload.size);
record_rtmp_publish_cancel(p);
assert(record_rtmp_publish_step(p) == RECORD_RTMP_PUBLISH_ERROR);
assert(record_rtmp_publish_get(p, &again));
assert(again.payload.data == m.payload.data && !memcmp(again.payload.data, saved, m.payload.size));
assert(!record_rtmp_publish_free(p) && record_rtmp_publish_ack(p));
assert(record_rtmp_publish_free(p));
p = make_publish(false);
output(p, "connect", 0);
m.payload.data = connected;
m.payload.size = sizeof(connected) - 1;
m.type = 20;
m.stream_id = m.timestamp = 0;
m.chunk_id = 3;
assert(record_rtmp_publish_reply(p, &m));
assert(record_rtmp_publish_step(p) == RECORD_RTMP_PUBLISH_PROGRESS);
record_rtmp_publish_cancel(p);
assert(!record_rtmp_publish_free(p));
assert(record_rtmp_publish_step(p) == RECORD_RTMP_PUBLISH_RELEASE_REPLY);
assert(record_rtmp_publish_step(p) == RECORD_RTMP_PUBLISH_ERROR);
assert(record_rtmp_publish_free(p));
}
static void failures(void)
{
record_rtmp_publish_t *p = make_publish(false);
enum record_rtmp_publish_error error;
uint8_t reply[sizeof(created)];
output(p, "connect", 0);
input(p, connection_error, sizeof(connection_error) - 1, 0);
assert(record_rtmp_publish_status(p, NULL, &error) == RECORD_RTMP_PUBLISH_FAILED);
assert(error == RECORD_RTMP_PUBLISH_ERROR_REJECTED && record_rtmp_publish_free(p));
p = make_publish(false);
output(p, "connect", 0);
input(p, connected, sizeof(connected) - 1, 0);
output(p, "createStream", 0);
memcpy(reply, created, sizeof(reply));
memset(reply + 21, 0, 8);
input(p, reply, sizeof(reply), 0);
assert(record_rtmp_publish_status(p, NULL, &error) == RECORD_RTMP_PUBLISH_FAILED);
assert(error == RECORD_RTMP_PUBLISH_ERROR_PROTOCOL && record_rtmp_publish_free(p));
p = make_publish(false);
output(p, "connect", 0);
input(p, "\377", 1, 0);
assert(record_rtmp_publish_step(p) == RECORD_RTMP_PUBLISH_ERROR);
assert(record_rtmp_publish_free(p));
assert(!record_rtmp_publish_new(NULL) && record_rtmp_publish_free(NULL));
}
int main(void)
{
lifecycle(false);
lifecycle(true);
cancellation();
failures();
puts("PASS: RTMP publish negotiation, stale replies, rejection and cancellation borrows");
return 0;
}
@@ -0,0 +1,166 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "../../../record/record_rtmp_receive.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
static const uint8_t wire[] = { 3, 0, 0, 10, 0, 0, 2, 20, 1, 0, 0, 0, 97, 98,
67, 0, 0, 5, 0, 0, 1, 20, 99, 131, 0, 0, 7, 100, 195, 101 };
static uint8_t large[5012];
static void basic(void)
{
record_rtmp_receive_t *r = record_rtmp_receive_new(32, 2);
record_rtmp_message_t m, again;
const uint8_t expected[] = { 97, 98, 99, 100, 101 };
const uint32_t times[] = { 10, 15, 22, 29 };
size_t pos = 0, used, offset = 0, n, ignored;
unsigned count = 0;
int result;
assert(r);
while (pos < sizeof(wire))
{
result = record_rtmp_receive_feed(r, wire + pos, 1, &used);
assert(result >= 0 && used == 1);
pos += used;
if (result != RECORD_RTMP_RECEIVE_MESSAGE)
continue;
assert(count < 4 && record_rtmp_receive_get(r, &m));
n = count ? 1 : 2;
assert(m.timestamp == times[count] && m.type == 20 && m.stream_id == 1 && m.chunk_id == 3);
assert(m.payload.size == n && !memcmp(m.payload.data, expected + offset, n));
assert(!record_rtmp_receive_finish(r));
assert(record_rtmp_receive_feed(r, wire, sizeof(wire), &ignored) == RECORD_RTMP_RECEIVE_MESSAGE);
assert(!ignored && record_rtmp_receive_get(r, &again));
assert(again.payload.data == m.payload.data && !memcmp(again.payload.data, expected + offset, n));
assert(record_rtmp_receive_release(r));
offset += n;
count++;
}
assert(count == 4 && record_rtmp_receive_finish(r));
record_rtmp_receive_free(r);
}
static void invalid(void)
{
record_rtmp_receive_t *r;
size_t used, i, pos;
int result = 0;
assert(!record_rtmp_receive_new(0, 1));
assert(!record_rtmp_receive_new(1, 65));
r = record_rtmp_receive_new(1, 1);
assert(r);
for (pos = 0; pos < sizeof(wire) && result >= 0; pos += used)
result = record_rtmp_receive_feed(r, wire + pos, sizeof(wire) - pos, &used);
assert(result == RECORD_RTMP_RECEIVE_ERROR && !record_rtmp_receive_finish(r));
assert(record_rtmp_receive_feed(r, NULL, 0, &used) == RECORD_RTMP_RECEIVE_ERROR && !used);
record_rtmp_receive_free(r);
for (i = 1; i < 14; i++)
{
r = record_rtmp_receive_new(32, 1);
assert(r);
for (pos = 0; pos < i; pos += used)
assert(record_rtmp_receive_feed(r, wire + pos, i - pos, &used) >= 0 && used);
assert(!record_rtmp_receive_finish(r));
record_rtmp_receive_free(r);
}
r = record_rtmp_receive_new(32, 1);
assert(r && !record_rtmp_receive_size(r, 0));
assert(record_rtmp_receive_feed(r, "\303", 1, &used) == RECORD_RTMP_RECEIVE_ERROR);
record_rtmp_receive_free(r);
}
static void write_uint(FILE *f, uint32_t value)
{
uint8_t p[4];
p[0] = (uint8_t)(value >> 24);
p[1] = (uint8_t)(value >> 16);
p[2] = (uint8_t)(value >> 8);
p[3] = (uint8_t)value;
assert(fwrite(p, 1, 4, f) == 4);
}
static void limits_and_abort(void)
{
record_rtmp_receive_t *r = record_rtmp_receive_new(5000, 1);
record_rtmp_message_t m;
size_t used, pos;
uint8_t changed[sizeof(wire)];
static const uint8_t partial[] = { 3, 0, 0, 0, 0, 0, 8, 20, 1, 0, 0, 0, 1, 2, 3, 4 };
assert(r && record_rtmp_receive_size(r, 8192));
large[0] = 3;
large[5] = 19;
large[6] = 136;
large[7] = 20;
assert(record_rtmp_receive_feed(r, large, sizeof(large), &used) == RECORD_RTMP_RECEIVE_MORE && used == 1);
assert(record_rtmp_receive_feed(r, large + 1, sizeof(large) - 1, &used) == RECORD_RTMP_RECEIVE_MORE && used == 11);
assert(record_rtmp_receive_feed(r, large + 12, 5000, &used) == RECORD_RTMP_RECEIVE_MORE && used == 4096);
assert(record_rtmp_receive_feed(r, large + 4108, 904, &used) == RECORD_RTMP_RECEIVE_MESSAGE && used == 904);
assert(record_rtmp_receive_get(r, &m) && m.payload.size == 5000);
assert(record_rtmp_receive_release(r));
memcpy(changed, wire, sizeof(wire));
changed[0] = 4;
assert(record_rtmp_receive_feed(r, changed, sizeof(changed), &used) == RECORD_RTMP_RECEIVE_ERROR);
record_rtmp_receive_free(r);
r = record_rtmp_receive_new(32, 1);
assert(r && record_rtmp_receive_size(r, 4));
for (pos = 0; pos < sizeof(partial); pos += used)
assert(record_rtmp_receive_feed(r, partial + pos, sizeof(partial) - pos, &used) == RECORD_RTMP_RECEIVE_MORE && used);
assert(!record_rtmp_receive_finish(r));
assert(record_rtmp_receive_abort(r, 3));
assert(record_rtmp_receive_finish(r));
record_rtmp_receive_free(r);
}
static void decode(const char *input, const char *output)
{
record_rtmp_receive_t *r = record_rtmp_receive_new(8192, 16);
record_rtmp_message_t m;
uint8_t buffer[257];
size_t n, pos, used;
FILE *in = fopen(input, "rb");
FILE *out = fopen(output, "wb");
int result;
assert(r && in && out);
while ((n = fread(buffer, 1, sizeof(buffer), in)) != 0)
for (pos = 0; pos < n; pos += used)
{
result = record_rtmp_receive_feed(r, buffer + pos, n - pos, &used);
assert(result >= 0 && used);
if (result != RECORD_RTMP_RECEIVE_MESSAGE)
continue;
assert(record_rtmp_receive_get(r, &m));
write_uint(out, m.chunk_id);
write_uint(out, m.stream_id);
write_uint(out, m.type);
write_uint(out, m.timestamp);
write_uint(out, (uint32_t)m.payload.size);
assert(fwrite(m.payload.data, 1, m.payload.size, out) == m.payload.size);
if (m.type == 1)
{
const uint8_t *p = (const uint8_t*)m.payload.data;
assert(m.payload.size == 4);
assert(record_rtmp_receive_size(r, ((unsigned)p[0] << 24) | ((unsigned)p[1] << 16)
| ((unsigned)p[2] << 8) | p[3]));
}
assert(record_rtmp_receive_release(r));
}
assert(!ferror(in) && !fclose(in) && !fclose(out));
assert(record_rtmp_receive_finish(r));
record_rtmp_receive_free(r);
}
int main(int argc, char **argv)
{
if (argc == 3)
{
decode(argv[1], argv[2]);
return 0;
}
assert(argc == 1);
basic();
invalid();
limits_and_abort();
puts("PASS: RTMP receive compression, borrowed output, bounds and truncation");
return 0;
}
+124
View File
@@ -0,0 +1,124 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "../../../record/record_rtmp_reply.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const uint8_t stream_reply[] =
{ 2, 0, 7, '_', 'r', 'e', 's', 'u', 'l', 't',
0, 64, 0, 0, 0, 0, 0, 0, 0, 5, 0, 65, 239, 255, 255, 255, 224, 0, 0 };
static int parse(record_rtmp_reply_t *r, size_t size)
{
size_t i;
int result = RECORD_RTMP_REPLY_PROGRESS;
for (i = 0; i < size * 3 + 16 && result == RECORD_RTMP_REPLY_PROGRESS; i++)
result = record_rtmp_reply_step(r);
assert(result != RECORD_RTMP_REPLY_PROGRESS);
return result;
}
static void contract(void)
{
record_rtmp_reply_t *r = record_rtmp_reply_new();
record_rtmp_reply_info_t info;
uint8_t wrapped[sizeof(stream_reply) + 1];
size_t i;
assert(r && !record_rtmp_reply_get(r, &info));
assert(record_rtmp_reply_begin(r, 20, stream_reply, sizeof(stream_reply)));
assert(!record_rtmp_reply_begin(r, 20, stream_reply, sizeof(stream_reply)));
assert(parse(r, sizeof(stream_reply)) == RECORD_RTMP_REPLY_DONE);
assert(record_rtmp_reply_get(r, &info));
assert(info.command.data == stream_reply + 3 && info.command.size == 7);
assert(info.transaction == 2 && info.stream_valid && info.stream == 0xffffffff);
assert(record_rtmp_reply_step(r) == RECORD_RTMP_REPLY_DONE);
record_rtmp_reply_release(r);
wrapped[0] = 0;
memcpy(wrapped + 1, stream_reply, sizeof(stream_reply));
assert(record_rtmp_reply_begin(r, 17, wrapped, sizeof(wrapped)));
assert(parse(r, sizeof(wrapped)) == RECORD_RTMP_REPLY_DONE);
record_rtmp_reply_release(r);
wrapped[0] = 1;
assert(!record_rtmp_reply_begin(r, 17, wrapped, sizeof(wrapped)));
for (i = 1; i < sizeof(stream_reply); i++)
{
assert(record_rtmp_reply_begin(r, 20, stream_reply, i));
if (i == 20)
assert(parse(r, i) == RECORD_RTMP_REPLY_DONE);
else
{
assert(parse(r, i) == RECORD_RTMP_REPLY_ERROR);
assert(record_rtmp_reply_step(r) == RECORD_RTMP_REPLY_ERROR);
assert(!record_rtmp_reply_get(r, &info));
}
record_rtmp_reply_release(r);
}
assert(record_rtmp_reply_begin(r, 20, stream_reply, sizeof(stream_reply)));
record_rtmp_reply_release(r);
record_rtmp_reply_free(r);
}
static void write_uint(FILE *f, uint32_t value)
{
uint8_t p[4];
p[0] = (uint8_t)(value >> 24);
p[1] = (uint8_t)(value >> 16);
p[2] = (uint8_t)(value >> 8);
p[3] = (uint8_t)value;
assert(fwrite(p, 1, 4, f) == 4);
}
static void write_span(FILE *f, record_rtmp_span_t span)
{
write_uint(f, (uint32_t)span.size);
if (span.size)
assert(fwrite(span.data, 1, span.size, f) == span.size);
}
static void decode(const char *input, const char *output)
{
record_rtmp_reply_t *r = record_rtmp_reply_new();
record_rtmp_reply_info_t info;
FILE *f = fopen(input, "rb");
uint8_t *data;
long size;
bool valid;
assert(r && f && !fseek(f, 0, SEEK_END));
size = ftell(f);
assert(size > 1 && size <= 65537 && !fseek(f, 0, SEEK_SET));
data = (uint8_t*)malloc((size_t)size);
assert(data && fread(data, 1, (size_t)size, f) == (size_t)size && !fclose(f));
valid = record_rtmp_reply_begin(r, data[0], data + 1, (size_t)size - 1)
&& parse(r, (size_t)size) == RECORD_RTMP_REPLY_DONE;
f = fopen(output, "wb");
assert(f && fputc(valid ? 1 : 0, f) != EOF);
if (valid)
{
assert(record_rtmp_reply_get(r, &info));
write_uint(f, info.transaction);
write_uint(f, info.stream);
assert(fputc(info.stream_valid ? 1 : 0, f) != EOF);
write_span(f, info.command);
write_span(f, info.code);
write_span(f, info.level);
write_span(f, info.description);
}
assert(!fclose(f));
record_rtmp_reply_release(r);
free(data);
record_rtmp_reply_free(r);
}
int main(int argc, char **argv)
{
if (argc == 3)
{
decode(argv[1], argv[2]);
return 0;
}
assert(argc == 1);
contract();
puts("PASS: bounded AMF reply lifecycle, exact IDs, wrapper and truncation");
return 0;
}
@@ -0,0 +1,221 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "../../../record/record_rtmp_session.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
#ifdef _WIN32
#include <io.h>
#include <fcntl.h>
#endif
static uint8_t random_bytes[1528], input[65536], output[65536];
static size_t output_size;
static record_rtmp_session_t *make_session(const char *url, bool legacy)
{
record_rtmp_publish_config_t c;
unsigned i;
c.application = "live";
c.url = url;
c.name = "test";
c.legacy_commands = legacy;
for (i = 0; i < sizeof(random_bytes); i++)
random_bytes[i] = (uint8_t)(i * 17 + 3);
return record_rtmp_session_new(&c, random_bytes, 0, sizeof(input), 8);
}
static bool pump(record_rtmp_session_t *s)
{
unsigned iteration;
bool any = false;
for (iteration = 0; iteration < 100000; iteration++)
{
record_rtmp_span_t span, again;
bool progress = record_rtmp_session_step(s);
if (record_rtmp_session_get(s, &span))
{
size_t take = span.size < 11 ? span.size : 11;
assert(record_rtmp_session_consume(s, 0));
assert(record_rtmp_session_get(s, &again));
assert(span.data == again.data && span.size == again.size);
assert(output_size + take <= sizeof(output));
memcpy(output + output_size, span.data, take);
output_size += take;
assert(record_rtmp_session_consume(s, take));
progress = true;
}
any = any || progress;
if (!progress)
return any;
}
assert(!"session made unbounded local progress");
return false;
}
static void write_uint(uint32_t value)
{
uint8_t bytes[4];
bytes[0] = (uint8_t)(value >> 24);
bytes[1] = (uint8_t)(value >> 16);
bytes[2] = (uint8_t)(value >> 8);
bytes[3] = (uint8_t)value;
assert(fwrite(bytes, 1, 4, stdout) == 4);
}
static void emit(record_rtmp_session_t *s)
{
pump(s);
write_uint((uint32_t)record_rtmp_session_status(s, NULL, NULL));
write_uint((uint32_t)output_size);
assert(fwrite(output, 1, output_size, stdout) == output_size);
output_size = 0;
assert(!fflush(stdout));
}
static void peer(const char *url, bool legacy)
{
record_rtmp_session_t *s = make_session(url, legacy);
uint8_t header[4];
size_t size, at, used;
#ifdef _WIN32
assert(_setmode(_fileno(stdin), _O_BINARY) != -1);
assert(_setmode(_fileno(stdout), _O_BINARY) != -1);
#endif
assert(s);
emit(s);
while (record_rtmp_session_status(s, NULL, NULL) != RECORD_RTMP_SESSION_READY)
{
assert(fread(header, 1, 4, stdin) == 4);
size = ((uint32_t)header[0] << 24) | ((uint32_t)header[1] << 16)
| ((uint32_t)header[2] << 8) | header[3];
assert(size && size <= sizeof(input));
assert(fread(input, 1, size, stdin) == size);
for (at = 0; at < size; at += used)
{
size_t take = size - at < 7 ? size - at : 7;
assert(record_rtmp_session_feed(s, input + at, take, &used, 0));
if (!used)
assert(pump(s));
else
pump(s);
}
emit(s);
}
assert(record_rtmp_session_free(s));
}
static void cancellation(void)
{
record_rtmp_session_t *s = make_session("rtmp://localhost/live", false);
record_rtmp_span_t span, again;
uint64_t sent;
assert(s && record_rtmp_session_get(s, &span));
assert(!record_rtmp_session_discard(s));
assert(record_rtmp_session_consume(s, 7));
record_rtmp_session_cancel(s);
assert(!record_rtmp_session_free(s));
assert(record_rtmp_session_get(s, &again));
assert(again.data == (const uint8_t*)span.data + 7);
assert(again.size == span.size - 7);
assert(record_rtmp_session_status(s, NULL, &sent) == RECORD_RTMP_SESSION_FAILED);
assert(sent == 7);
assert(record_rtmp_session_discard(s));
assert(!record_rtmp_session_get(s, &again));
assert(record_rtmp_session_free(s));
assert(!record_rtmp_session_new(NULL, random_bytes, 0, 4096, 8));
assert(!record_rtmp_session_new(NULL, NULL, 0, 0, 0));
}
static record_rtmp_session_t *negotiating(void)
{
record_rtmp_session_t *s = make_session("rtmp://localhost/live", false);
record_rtmp_span_t span;
size_t at, used;
assert(s && record_rtmp_session_get(s, &span));
assert(span.size == 1537);
memset(input, 0, 3073);
input[0] = 3;
memcpy(input + 1537, (const uint8_t*)span.data + 1, 1536);
assert(record_rtmp_session_consume(s, span.size));
for (at = 0; at < 3073; at += used)
{
assert(record_rtmp_session_feed(s, input + at, 3073 - at, &used, 0));
assert(used);
}
assert(record_rtmp_session_get(s, &span) && span.size == 1536);
assert(record_rtmp_session_consume(s, span.size));
assert(record_rtmp_session_step(s));
assert(record_rtmp_session_status(s, NULL, NULL) == RECORD_RTMP_SESSION_NEGOTIATING);
assert(record_rtmp_session_step(s));
return s;
}
static void internal_retirement(void)
{
static const uint8_t connected[] =
"\2\0\7_result\0\77\360\0\0\0\0\0\0\5\3"
"\0\4code\2\0\35NetConnection.Connect.Success\0\0\11";
record_rtmp_session_t *s = negotiating();
record_rtmp_span_t span;
size_t at, used, size = sizeof(connected) - 1;
assert(record_rtmp_session_get(s, &span));
assert(!record_rtmp_session_free(s));
record_rtmp_session_cancel(s);
assert(record_rtmp_session_discard(s));
assert(record_rtmp_session_free(s));
s = negotiating();
output_size = 0;
pump(s);
memset(input, 0, 12);
input[0] = 3;
input[6] = (uint8_t)size;
input[7] = 20;
memcpy(input + 12, connected, size);
for (at = 0; at < size + 12; at += used)
{
assert(record_rtmp_session_feed(s, input + at, size + 12 - at, &used, 0));
assert(used);
}
assert(record_rtmp_session_step(s));
record_rtmp_session_cancel(s);
assert(record_rtmp_session_free(s));
output_size = 0;
}
static void rejection(void)
{
static const uint8_t rejected[] = "\2\0\6_error\0\77\360\0\0\0\0\0\0\5";
record_rtmp_session_t *s = negotiating();
size_t at, used, size = sizeof(rejected) - 1;
output_size = 0;
pump(s);
memset(input, 0, 12);
input[0] = 3;
input[6] = (uint8_t)size;
input[7] = 20;
memcpy(input + 12, rejected, size);
for (at = 0; at < size + 12; at += used)
{
assert(record_rtmp_session_feed(s, input + at, size + 12 - at, &used, 0));
assert(used);
}
pump(s);
assert(record_rtmp_session_status(s, NULL, NULL) == RECORD_RTMP_SESSION_FAILED);
assert(record_rtmp_session_free(s));
output_size = 0;
}
int main(int argc, char **argv)
{
if (argc == 4 && !strcmp(argv[1], "--peer"))
{
peer(argv[2], !strcmp(argv[3], "legacy"));
return 0;
}
assert(argc == 1);
cancellation();
internal_retirement();
rejection();
puts("PASS RTMP session cancellation, partial output and failed initialization");
return 0;
}
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Decode transmitter fixtures independently using RTMP chunk grammar."""
import argparse
from pathlib import Path
import shlex
import subprocess
import tempfile
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--runner', default='')
parser.add_argument('binary')
args = parser.parse_args()
with tempfile.TemporaryDirectory(prefix='rtmp-chunk-') as temp:
output = Path(temp) / 'chunks.bin'
subprocess.run(shlex.split(args.runner) +
[str(Path(args.binary).resolve()), str(output)], check=True, timeout=30)
data = output.read_bytes()
pos = 0
def take(n):
nonlocal pos
assert n <= len(data) - pos, 'truncated chunk'
result = data[pos:pos + n]
pos += n
return result
def basic():
value = take(1)[0]
ident = value & 63
if ident == 0:
ident = 64 + take(1)[0]
elif ident == 1:
ident = 64 + int.from_bytes(take(2), 'little')
return value >> 6, ident
ids = (2, 63, 64, 319, 320, 65599)
times = (0, 0xfffffe, 0xffffff, 0x1000000, 0xffffffff, 42)
sizes = (0, 1, 127, 128, 129, 700)
for ident, timestamp, size in zip(ids, times, sizes):
assert basic() == (0, ident)
header = take(11)
stamp = int.from_bytes(header[:3], 'big')
assert stamp == min(timestamp, 0xffffff)
assert int.from_bytes(header[3:6], 'big') == size
assert header[6] == 9 and int.from_bytes(header[7:], 'little') == 0x12345678
if stamp == 0xffffff:
assert int.from_bytes(take(4), 'big') == timestamp
payload = bytearray()
while len(payload) < size:
if payload:
assert basic() == (3, ident)
if stamp == 0xffffff:
assert int.from_bytes(take(4), 'big') == timestamp
payload.extend(take(min(128, size - len(payload))))
assert payload == bytes((i * 37 + 11) & 255 for i in range(size))
assert pos == len(data), 'trailing data'
print('PASS: independent RTMP chunk grammar and payload oracle')
if __name__ == '__main__':
main()
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Drive the production session through an independent RTMP publisher handshake."""
import argparse
import os
from pathlib import Path
import queue
import shlex
import socket
import struct
import subprocess
import tempfile
import threading
import time
def run(binary, runner, legacy):
ffmpeg = os.environ.get('FFMPEG', 'ffmpeg')
help_text = subprocess.run([ffmpeg, '-hide_banner', '-h', 'protocol=rtmp'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
check=True, timeout=10).stdout
if b'-rtmp_listen' not in help_text:
raise RuntimeError('FFMPEG must name a build with the native RTMP listener')
with socket.socket() as reserve:
reserve.bind(('127.0.0.1', 0))
port = reserve.getsockname()[1]
url = f'rtmp://127.0.0.1:{port}/live'
with tempfile.TemporaryFile() as server_log, tempfile.TemporaryFile() as client_log:
server = subprocess.Popen([ffmpeg, '-v', 'debug',
'-listen', '1', '-timeout', '10', '-i', url + '/test',
'-f', 'null', '-'], stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL, stderr=server_log)
client = None
try:
deadline = time.monotonic() + 20
while True:
try:
connection = socket.create_connection(('127.0.0.1', port), timeout=3)
break
except ConnectionRefusedError:
if server.poll() is not None or time.monotonic() >= deadline:
raise RuntimeError('RTMP listener did not start')
time.sleep(0.01)
client = subprocess.Popen(shlex.split(runner) + [str(Path(binary).resolve()),
'--peer', url, 'legacy' if legacy else 'standard'],
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=client_log)
frames = queue.Queue()
def read_frames():
try:
while True:
header = client.stdout.read(8)
if len(header) != 8:
raise EOFError('session bridge closed before READY')
state, size = struct.unpack('>II', header)
if size > 65536:
raise ValueError('unbounded session output')
data = client.stdout.read(size)
if len(data) != size:
raise EOFError('partial session output')
frames.put((state, data))
except Exception as error:
frames.put(error)
threading.Thread(target=read_frames, daemon=True).start()
with connection:
connection.settimeout(5)
for _ in range(4096):
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError('publish session deadline expired')
frame = frames.get(timeout=remaining)
if isinstance(frame, Exception):
raise frame
state, data = frame
if data:
connection.sendall(data)
if state == 0xffffffff:
raise RuntimeError('production session failed')
if state == 2:
assert client.wait(timeout=5) == 0
break
incoming = connection.recv(97)
if not incoming:
raise EOFError('independent receiver closed before publish readiness')
client.stdin.write(struct.pack('>I', len(incoming)) + incoming)
client.stdin.flush()
else:
raise RuntimeError('publish session exceeded fragment bound')
except Exception:
server_log.seek(0)
client_log.seek(0)
print(server_log.read().decode(errors='replace'))
print(client_log.read().decode(errors='replace'))
raise
finally:
if client:
if client.poll() is None:
client.kill()
client.wait(timeout=5)
client.stdin.close()
client.stdout.close()
if server.poll() is None:
server.terminate()
try:
server.wait(timeout=5)
except subprocess.TimeoutExpired:
server.kill()
server.wait(timeout=5)
print('PASS complete production RTMP publish session with FFmpeg receiver: ' +
('legacy commands' if legacy else 'standard commands'))
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--runner', default='')
parser.add_argument('binary')
args = parser.parse_args()
run(args.binary, args.runner, False)
run(args.binary, args.runner, True)