Browse Source

Remove unused media receiver and test the shared ring cursor

master
evgeny 4 days ago
parent
commit
d75aea1a09
  1. 98
      src/stream/stream_media.c
  2. 13
      src/stream/stream_media.h
  3. 141
      tests/test_stream_media.c

98
src/stream/stream_media.c

@ -1,19 +1,8 @@
#include <string.h>
#include "../../lib/debug_config.h"
#include "../../lib/mem.h"
#include "stream_media.h"
struct stream_media_receiver {
stream_media_frame_fn callback;
void *arg;
struct stream_media_format format;
struct stream_media_video frame;
uint8_t *data;
uint32_t used;
uint64_t last_video_pts, last_audio_pts, last_frame_id;
int configured, waiting_key, video_seen, audio_seen;
};
/* Проверить весь набор описаний до чтения payload или выделения памяти под кадр. */
static int media_validate(const struct stream_media_block *b, size_t *video_size, size_t *audio_size, uint32_t *first) {
@ -165,90 +154,3 @@ int stream_media_key_parse(const uint8_t *data, size_t length, const uint8_t **c
return STREAM_OK;
}
/* Receiver имеет один ограниченный буфер; ожидание KEY не мешает доставке независимого аудио. */
int stream_media_receiver_create(struct stream_media_receiver **out, stream_media_frame_fn callback, void *arg) {
if (!out || *out || !callback) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "media receiver create invalid arguments"); return STREAM_ERR_ARGUMENT; }
struct stream_media_receiver *r = u_calloc(1, sizeof(*r));
if (!r) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "media receiver allocation failed"); return STREAM_ERR_MEMORY; }
r->callback = callback; r->arg = arg; r->waiting_key = 1; *out = r;
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "media receiver created frame_limit=%u", STREAM_MEDIA_MAX_FRAME);
return STREAM_OK;
}
/* Сбросить недособранный кадр и зависимые P-кадры; PTS-история сохраняется в пределах эпохи. */
void stream_media_receiver_gap(struct stream_media_receiver *r) {
if (!r) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "media receiver gap missing context"); return; }
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "media receiver GAP frame=%llu used=%u/%u; waiting for KEY",
(unsigned long long)r->frame.frame_id, r->used, r->frame.frame_size);
u_free(r->data); r->data = NULL; r->used = 0; r->waiting_key = 1;
}
/* Только полный последовательный кадр допускается в декодер; никакого склеивания через GAP. */
int stream_media_receiver_feed(struct stream_media_receiver *r, const uint8_t *data, size_t length) {
if (!r) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "media receiver feed missing context"); return STREAM_ERR_ARGUMENT; }
struct stream_media_block b;
int result = stream_media_parse(data, length, &b);
if (result != STREAM_OK) { stream_media_receiver_gap(r); return result; }
if (r->configured && b.format.revision < r->format.revision) {
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "media stale revision=%u current=%u", b.format.revision, r->format.revision);
return STREAM_OK;
}
if (r->configured && (r->format.audio_codec != b.format.audio_codec
|| (r->format.revision == b.format.revision && (r->format.video_codec != b.format.video_codec || r->format.width != b.format.width
|| r->format.height != b.format.height || r->format.rotation != b.format.rotation)))) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "media conflicting configuration revision=%u/%u dimensions=%ux%u/%ux%u rotation=%u/%u audio=%u/%u",
r->format.revision, b.format.revision, r->format.width, r->format.height, b.format.width, b.format.height,
r->format.rotation, b.format.rotation, r->format.audio_codec, b.format.audio_codec);
stream_media_receiver_gap(r); return STREAM_ERR_CONFLICT;
}
if (r->configured && b.format.revision > r->format.revision) {
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "media video configuration revision=%u->%u dimensions=%ux%u rotation=%u; preserve audio",
r->format.revision, b.format.revision, b.format.width, b.format.height, b.format.rotation);
stream_media_receiver_gap(r);
}
r->format = b.format; r->configured = 1;
for (unsigned i = 0; i < b.video_count; i++) {
const struct stream_media_video *v = &b.video[i];
if (!v->offset) {
if (r->data) stream_media_receiver_gap(r);
if (r->video_seen && (v->pts_us <= r->last_video_pts || v->frame_id <= r->last_frame_id)) {
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "media stale video frame=%llu", (unsigned long long)v->frame_id); continue;
}
if (r->waiting_key && !(v->flags & STREAM_MEDIA_KEY)) continue;
r->data = u_malloc(v->frame_size);
if (!r->data) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "media frame allocation failed bytes=%u", v->frame_size); return STREAM_ERR_MEMORY; }
r->frame = *v; r->used = 0;
}
if (!r->data) continue;
if (v->frame_id != r->frame.frame_id || v->pts_us != r->frame.pts_us || v->frame_size != r->frame.frame_size
|| v->flags != r->frame.flags || v->offset != r->used) {
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "media fragment discontinuity frame=%llu offset=%u expected=%u",
(unsigned long long)v->frame_id, v->offset, r->used);
stream_media_receiver_gap(r); continue;
}
memcpy(r->data + r->used, v->data, v->length); r->used += v->length;
if (r->used == r->frame.frame_size) {
r->frame.data = r->data; r->frame.length = r->used; r->frame.offset = 0;
if (r->waiting_key) DEBUG_INFO(DEBUG_CATEGORY_STREAM, "media decoder resumes KEY frame=%llu pts=%llu bytes=%u",
(unsigned long long)v->frame_id, (unsigned long long)v->pts_us, r->used);
r->waiting_key = 0; r->video_seen = 1; r->last_video_pts = v->pts_us; r->last_frame_id = v->frame_id;
r->callback(r->arg, &r->format, &r->frame, NULL);
u_free(r->data); r->data = NULL; r->used = 0;
}
}
for (unsigned i = 0; i < b.audio_count; i++) {
const struct stream_media_audio *a = &b.audio[i];
if (r->audio_seen && a->pts_us <= r->last_audio_pts) {
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "media stale audio pts=%llu", (unsigned long long)a->pts_us); continue;
}
r->audio_seen = 1; r->last_audio_pts = a->pts_us; r->callback(r->arg, &r->format, NULL, a);
}
return STREAM_OK;
}
/* Освободить только собственный буфер, не заимствованные данные callback. */
void stream_media_receiver_destroy(struct stream_media_receiver *r) {
if (!r) return;
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "media receiver destroy pending=%u waiting_key=%d", r->used, r->waiting_key);
u_free(r->data); u_free(r);
}

13
src/stream/stream_media.h

@ -5,8 +5,8 @@
* KEY payload: config_length:4BE, codec configuration, access unit. Остальные кадры — access unit.
* Видео меняется по revision; аудиотрек фиксирован на source_epoch. Конфигурация защищена общей подписью/RS.
* first_frame_offset относится к video bytes; UINT32_MAX означает отсутствие начала кадра.
* Parser проверяет весь блок до публикации view. Receiver собирает один кадр, после GAP ждёт KEY.
* Все вызовы одного контекста последовательны; callbacks заимствуют данные до возврата, без reentry. */
* Parser проверяет весь блок до публикации view, указатели view заимствованы из входного блока.
* Сборка кадров и восстановление после GAP/revision выполняются store/cursor из stream_media_buffer.h. */
#ifndef STREAM_MEDIA_H
#define STREAM_MEDIA_H
@ -56,15 +56,6 @@ int stream_media_parse(const uint8_t *data, size_t length, struct stream_media_b
int stream_media_key_parse(const uint8_t *data, size_t length, const uint8_t **config, size_t *config_length,
const uint8_t **frame, size_t *frame_length);
struct stream_media_receiver;
/* video=NULL означает один Opus-пакет; audio=NULL — полный H.264 кадр. */
typedef void (*stream_media_frame_fn)(void *arg, const struct stream_media_format *format,
const struct stream_media_video *video, const struct stream_media_audio *audio);
int stream_media_receiver_create(struct stream_media_receiver **out, stream_media_frame_fn callback, void *arg);
void stream_media_receiver_gap(struct stream_media_receiver *receiver);
int stream_media_receiver_feed(struct stream_media_receiver *receiver, const uint8_t *data, size_t length);
void stream_media_receiver_destroy(struct stream_media_receiver *receiver);
#ifdef __cplusplus
}
#endif

141
tests/test_stream_media.c

@ -11,31 +11,23 @@
#define CHECK(x) do { if (!(x)) { debug_set_category_level(DEBUG_CATEGORY_STREAM, DEBUG_LEVEL_ERROR); DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "stream media test line=%d: %s", __LINE__, #x); exit(1); } } while (0)
static unsigned video_frames, audio_frames, batches;
static unsigned batches;
static uint8_t expected_video[9000];
static void frame(void *arg, const struct stream_media_format *format,
const struct stream_media_video *video, const struct stream_media_audio *audio) {
(void)arg; CHECK(format->width == 1280 && format->height == 720);
if (video) { CHECK(video->length == sizeof(expected_video) && !memcmp(video->data, expected_video, video->length)); video_frames++; }
if (audio) { CHECK(audio->length == 3 && !memcmp(audio->data, "ops", 3)); audio_frames++; }
}
static void ready(struct stream_source *source, uint64_t seq, void *arg) {
(void)source; (void)arg; CHECK(seq == batches++);
}
/* Канонический формат и сохранение выходного view при ошибке всех классов полей. */
static void test_media(void) {
struct stream_media_receiver *receiver = NULL;
CHECK(stream_media_receiver_create(&receiver, frame, NULL) == STREAM_OK);
struct stream_media_block b = {.format = {1280, 720, 90, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS, 1}, .video_count = 1, .audio_count = 1};
b.video[0] = (struct stream_media_video){1, 1000000, sizeof(expected_video), 0, 6000, STREAM_MEDIA_KEY, expected_video};
b.audio[0] = (struct stream_media_audio){1000000, 3, 960, (const uint8_t *)"ops"};
uint8_t wire[7080], saved[7080]; size_t length = 0;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(receiver, wire, length) == STREAM_OK && video_frames == 0 && audio_frames == 1);
memcpy(saved, wire, length);
struct stream_media_block parsed;
CHECK(stream_media_parse(wire, length, &parsed) == STREAM_OK && parsed.first_frame_offset == 0);
CHECK(parsed.audio_count == 1 && parsed.audio[0].length == 3 && !memcmp(parsed.audio[0].data, "ops", 3));
const unsigned mutations[] = {0, 4, 5, 6, 7, 8, 12, 14, 16, 20, 24, 48, 52, 56, 60, 61, 72, 76, 78};
for (unsigned i = 0; i < sizeof(mutations) / sizeof(mutations[0]); i++) {
memcpy(wire, saved, length); wire[mutations[i]] = 255;
@ -47,33 +39,11 @@ static void test_media(void) {
b.audio_count = 0;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_parse(wire, length, &parsed) == STREAM_OK && parsed.first_frame_offset == UINT32_MAX);
CHECK(stream_media_receiver_feed(receiver, wire, length) == STREAM_OK && video_frames == 1);
stream_media_receiver_gap(receiver);
CHECK(stream_media_receiver_feed(receiver, wire, length) == STREAM_OK && video_frames == 1);
b.video[0] = (struct stream_media_video){2, 2000000, sizeof(expected_video), 0, 6000, 0, expected_video};
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(receiver, wire, length) == STREAM_OK && video_frames == 1);
b.video[0].flags = STREAM_MEDIA_KEY;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(receiver, wire, length) == STREAM_OK);
stream_media_receiver_gap(receiver);
b.video[0].offset = 6000; b.video[0].length = 3000; b.video[0].data = expected_video + 6000;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(receiver, wire, length) == STREAM_OK && video_frames == 1);
b.video_count = 2;
b.video[1] = (struct stream_media_video){3, 3000000, sizeof(expected_video), 0, 3000, STREAM_MEDIA_KEY, expected_video};
b.video[1] = (struct stream_media_video){2, 1040000, sizeof(expected_video), 0, 3000, STREAM_MEDIA_KEY, expected_video};
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_parse(wire, length, &parsed) == STREAM_OK && parsed.first_frame_offset == 3000);
CHECK(stream_media_receiver_feed(receiver, wire, length) == STREAM_OK && video_frames == 1);
b.video_count = 1; b.video[0] = b.video[1];
b.video[0].offset = 3000; b.video[0].length = 6000; b.video[0].data = expected_video + 3000;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(receiver, wire, length) == STREAM_OK && video_frames == 2);
b.format.width++;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(receiver, wire, length) == STREAM_ERR_CONFLICT);
stream_media_receiver_destroy(receiver);
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "PASS media: large frames, nonzero start, malformed fields, GAP and format changes");
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "PASS media: nonzero frame start, malformed fields and atomic parser output");
}
/* Две короткие записи остаются двумя пачками; каждый медиазаголовок переживает любую пару потерь 6+2. */
@ -116,76 +86,7 @@ static void test_batches(void) {
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "PASS batches: atomic boundaries, all FEC losses and mode isolation");
}
static unsigned revised_videos, revised_audios;
static uint32_t received_revision;
static void revised_frame(void *arg, const struct stream_media_format *format,
const struct stream_media_video *video, const struct stream_media_audio *audio) {
(void)arg;
if (video) { revised_videos++; received_revision = format->revision; CHECK(video->flags & STREAM_MEDIA_KEY); }
if (audio) revised_audios++;
}
/* Revision переживает потерю первого IDR; смена видео не сбрасывает PTS аудио и кадров. */
static void test_revisions(void) {
struct stream_media_receiver *r = NULL;
CHECK(stream_media_receiver_create(&r, revised_frame, NULL) == STREAM_OK);
struct stream_media_block b = {.format = {1280, 720, 90, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS, 1}, .video_count = 1};
b.video[0] = (struct stream_media_video){1, 1000000, 12, 0, 6, STREAM_MEDIA_KEY, expected_video};
uint8_t wire[7080]; size_t length;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(r, wire, length) == STREAM_OK && revised_videos == 0);
b.format = (struct stream_media_format){1920, 1080, 270, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS, 2};
b.video_count = 0; b.audio_count = 1;
b.audio[0] = (struct stream_media_audio){1000000, 3, 960, (const uint8_t *)"ops"};
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(r, wire, length) == STREAM_OK && revised_audios == 1);
b.video_count = 1; b.audio_count = 0;
b.video[0] = (struct stream_media_video){2, 1100000, 12, 0, 12, 0, expected_video};
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(r, wire, length) == STREAM_OK && revised_videos == 0);
b.video[0] = (struct stream_media_video){3, 1200000, 12, 0, 6, STREAM_MEDIA_KEY, expected_video};
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(r, wire, length) == STREAM_OK);
stream_media_receiver_gap(r);
b.video[0].offset = 6; b.video[0].data = expected_video + 6;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(r, wire, length) == STREAM_OK && revised_videos == 0);
b.video[0] = (struct stream_media_video){4, 1300000, 12, 0, 12, STREAM_MEDIA_KEY, expected_video};
b.audio_count = 1; b.audio[0].pts_us = 1020000;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(r, wire, length) == STREAM_OK && revised_videos == 1 && revised_audios == 2 && received_revision == 2);
b.format.revision = 1; b.video[0].frame_id = 5; b.video[0].pts_us = 1400000;
b.audio[0].pts_us = 1040000;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(r, wire, length) == STREAM_OK && revised_videos == 1 && revised_audios == 2);
b.format.revision = 4; b.video[0].flags = 0;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(r, wire, length) == STREAM_OK && revised_videos == 1 && revised_audios == 3);
b.video[0].frame_id = 6; b.video[0].pts_us = 1500000; b.video[0].flags = STREAM_MEDIA_KEY;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(r, wire, length) == STREAM_OK && revised_videos == 2 && received_revision == 4 && revised_audios == 3);
b.format.width++;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(r, wire, length) == STREAM_ERR_CONFLICT);
b.format.width--; b.format.revision = 5; b.format.audio_codec = 0; b.audio_count = 0;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(r, wire, length) == STREAM_ERR_CONFLICT);
b.format.audio_codec = STREAM_MEDIA_OPUS; b.format.revision = 6; b.video[0].pts_us = 1400000;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(r, wire, length) == STREAM_OK && revised_videos == 2);
memset(wire + 28, 0, 4);
struct stream_media_block parsed;
CHECK(stream_media_parse(wire, length, &parsed) == STREAM_ERR_FORMAT);
stream_media_receiver_destroy(r);
}
static void stereo_frame(void *arg, const struct stream_media_format *format,
const struct stream_media_video *video, const struct stream_media_audio *audio) {
CHECK(!video && audio && audio->samples == 960 && format->audio_codec == STREAM_MEDIA_OPUS_STEREO);
(*(unsigned *)arg)++;
}
/* Стерео обозначается в wire явно; 960 — число кадров на канал, трек не меняется внутри эпохи. */
/* Стерео обозначается в wire явно; 960 — число кадров на канал. */
static void test_stereo(void) {
struct stream_media_block b = {.format = {1280, 720, 0, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS_STEREO, 1}, .audio_count = 1};
b.audio[0] = (struct stream_media_audio){1000000, 3, 960, (const uint8_t *)"ops"};
@ -193,17 +94,11 @@ static void test_stereo(void) {
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
struct stream_media_block parsed;
CHECK(stream_media_parse(wire, length, &parsed) == STREAM_OK && parsed.format.audio_codec == STREAM_MEDIA_OPUS_STEREO);
unsigned received = 0; struct stream_media_receiver *receiver = NULL;
CHECK(stream_media_receiver_create(&receiver, stereo_frame, &received) == STREAM_OK);
CHECK(stream_media_receiver_feed(receiver, wire, length) == STREAM_OK && received == 1);
b.format.audio_codec = STREAM_MEDIA_OPUS; b.format.revision++; b.audio[0].pts_us += 20000;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_OK);
CHECK(stream_media_receiver_feed(receiver, wire, length) == STREAM_ERR_CONFLICT && received == 1);
b.format.audio_codec = STREAM_MEDIA_OPUS_STEREO; b.audio[0].samples = 1920;
CHECK(parsed.audio[0].samples == 960 && parsed.audio[0].length == 3 && !memcmp(parsed.audio[0].data, "ops", 3));
b.audio[0].samples = 1920;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_ERR_FORMAT);
b.audio[0].samples = 960; b.format.audio_codec = 3;
CHECK(stream_media_encode(&b, wire, sizeof(wire), &length) == STREAM_ERR_FORMAT);
stream_media_receiver_destroy(receiver);
}
static void test_codecs(void) {
@ -260,6 +155,24 @@ static int media_fixture_feed(struct media_fixture *f, const struct stream_media
return stream_media_store_feed(f->store, f->seq++, decoded, decoded_size);
}
/* Формат внутри revision и аудио внутри эпохи неизменны; регрессия live revision запрещена. */
static void test_format_conflicts(void) {
for (unsigned conflict = 0; conflict < 4; conflict++) {
struct media_fixture f; media_fixture_open(&f);
struct stream_media_block b = {.format = {1280, 720, 0, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS_STEREO, 2},
.video_count = 1};
b.video[0] = (struct stream_media_video){1, 1000000, 12, 0, 12, STREAM_MEDIA_KEY, expected_video};
CHECK(media_fixture_feed(&f, &b) == STREAM_OK);
if (conflict == 0) b.format.revision = 1;
else if (conflict == 1) b.format.width++;
else { b.format.revision++; b.format.audio_codec = conflict == 2 ? STREAM_MEDIA_OPUS : 0; }
b.video[0].frame_id++; b.video[0].pts_us += 40000;
CHECK(media_fixture_feed(&f, &b) == STREAM_ERR_CONFLICT);
media_fixture_close(&f);
}
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "PASS media store: revision regression, conflicting video and changed/removed audio rejected");
}
/* Новая revision начинается с целого KEY; аудио и отстающий reader сохраняют собственные позиции. */
static void test_revision_transition(void) {
struct media_fixture f; media_fixture_open(&f);
@ -361,9 +274,9 @@ int main(int argc, char **argv) {
test_revision_transition(); test_cursor_key_recovery();
} else {
CHECK(argc == 1);
test_media(); test_batches(); test_revisions(); test_stereo(); test_codecs();
test_media(); test_batches(); test_stereo(); test_codecs();
debug_set_category_level(DEBUG_CATEGORY_STREAM, DEBUG_LEVEL_INFO);
test_revision_transition(); test_cursor_key_recovery();
test_revision_transition(); test_cursor_key_recovery(); test_format_conflicts();
}
CHECK(u_get_allocated_count() == before);
debug_set_category_level(DEBUG_CATEGORY_STREAM, DEBUG_LEVEL_INFO);

Loading…
Cancel
Save