mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 08:36:02 -10:00
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:
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: |
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user