diff --git a/doc/stream.txt b/doc/stream.txt index e8a67556..f99c499f 100644 --- a/doc/stream.txt +++ b/doc/stream.txt @@ -754,7 +754,8 @@ KEY=bit0 video flags; остальные биты запрещены. frame_id>0 video_codec=1: AVC/H.264 Annex B, без B-кадров. Каждый KEY содержит SPS, PPS и IDR в одном логическом кадре; это позволяет позднему viewer настроить декодер после получения полного KEY. -audio_codec=0: нет аудио; audio_codec=1: Opus, 48000Hz mono, 960 samples (20ms), packet<=1275 bytes. +audio_codec=0: нет аудио; audio_codec=1: Opus mono, audio_codec=2: Opus stereo. +Оба формата: 48000Hz, 960 кадров на канал (20ms), packet<=1275 bytes; interleaved stereo содержит 1920 отсчётов PCM. Opus-пакеты между медиаблоками не разрезаются. Размеры треков не включают header/описания. stream_media проверяет весь блок до изменения view. Receiver собирает один последовательный кадр. @@ -774,10 +775,10 @@ Android: partial output собирается в access unit. Codec config передаётся с каждым IDR. - Camera MediaCodec Surface timestamps и AudioTimestamp.TIMEBASE_MONOTONIC приводятся к общему origin System.nanoTime; время callback/получения блока не заменяет capture PTS. -- Микрофон трансляции использует AudioRecord.CAMCORDER, 48kHz mono. Перед Opus AUDIO/64kbps +- Микрофон трансляции использует AudioRecord.CAMCORDER, 48kHz stereo. Перед Opus AUDIO/128kbps применяется тот же audio_compressor, что в TX рации: общие настройки включения, max_gain и rise_rate, lookback=200ms, lookahead=0, release=rise*5, target=0.25. История и gain принадлежат только audio worker - трансляции. Изменения настроек рации применяются на следующем PCM-кадре, без перезапуска микрофона, + трансляции; оба канала используют общий gain, сохраняющий стереобаланс. Изменения настроек рации применяются на следующем PCM-кадре, без перезапуска микрофона, Opus или source_epoch; compressor не добавляет задержку и не меняет capture PTS. - Camera/codec callbacks — HandlerThread, Opus encoder — audio capture thread, decoder/AudioTrack — viewer worker. - Native bridge держит pthread mutex, ограниченные очереди и один pending post либо timer. @@ -797,9 +798,9 @@ Android: монотонные часы. Короткие audio gaps заполняются Opus PLC, большой разрыв сбрасывает timeline. Локальный preview использует обычную подписку на собственный источник с отключённым воспроизведением своего микрофона, чтобы исключить акустическую обратную связь. Локальный preview ориентируется по сенсору своей камеры - и текущему положению экрана; ручной поворот трансляции меняет только метку «Верх», показывающую верх картинки зрителей. + и текущему положению экрана; верх картинки зрителей отмечается красной гранью на кнопках поворота. Положение камеры хранится per-revision отдельно от wire, чтобы queued кадры не использовали параметры новой камеры. - На SM проверено: при ручных 0/90/180/270 размеры и ориентация превью сохраняются, меняется только положение метки. + Ручной поворот сохраняет размеры и ориентацию локального превью; метка «Верх» на видео не рисуется. Удалённый viewer применяет video rotation источника без локальной метки. Камера при закрытии preview продолжает работать. Viewer закрывается на Activity ON_STOP и создаётся заново на ON_START; publisher от UI не зависит. Видимый player удерживает экран включённым. Preview ограничен оставшимся местом, кнопки управления не перекрывает. @@ -813,10 +814,11 @@ Android: несколько выбираются из списка по имени/ID. Пункт меню «Трансляции» также открывает управление публикацией. Выбранная эпоха проверяется actor до подписки и по фактически открытому handle. Перезапуск источника требует нового выбора. Ошибка viewer kind=6 передаёт stream_close_reason в flags. -- Ручная ориентация доступна до запуска и во время своей трансляции: 0/90/180/270 градусов относительно +- Ручная ориентация доступна до запуска и во время своей трансляции: три компактные кнопки 0/90/270 градусов относительно обычного положения устройства. Поворот не зависит от текущего положения экрана; SENSOR_ORIENTATION и направление камеры учитываются при расчёте video rotation. Камера/режим/ориентация хранятся и подтверждаются одной конфигурацией. При смене камеры выбранная ориентация сохраняется; rollback восстанавливает всю предыдущую конфигурацию. + Активная кнопка подсвечивается. Верх отмечается на самой кнопке с учётом передней/задней камеры и положения экрана. - Смена камеры/режима/ориентации сохраняет publisher owner, source_epoch, микрофон, origin и frame IDs. Camera callbacks и codec output отсеиваются по поколению. Новая камера открывается после onClosed предыдущей; close timeout=3s, KEY/подтверждение режима timeout=8s. @@ -860,7 +862,8 @@ Desktop: - transport/stream_bridge владеет одним viewer и ограничивает RX 4MiB/256 записей. Attach/detach выполняются в uasync; команды и копирование медиаданных защищены mutex. Post отменяется до destroy ядра; GUI/decoder не хранят заимствованные instance/handle pointers. -- stream_media_receiver собирает H.264 access units и выдаёт Opus 48kHz mono/20ms. +- stream_media_receiver собирает H.264 access units и выдаёт Opus 48kHz mono/stereo/20ms. + Android AudioTrack и desktop output работают в stereo; mono-пакеты upmix выполняет libopus. FFmpeg/Opus декодируются отдельным worker; Qt-очередь не накапливает кадры. Очередь декодированного видео ограничена 128 кадрами и 256MiB, аудио — 25 пакетами (500ms). Кадры сохраняются до достижения их PTS аудиочасами. При пределе видео отбрасывается новый diff --git a/tests/test_opus_codec.c b/tests/test_opus_codec.c index 0f2a5755..56e88391 100644 --- a/tests/test_opus_codec.c +++ b/tests/test_opus_codec.c @@ -211,6 +211,25 @@ static int test_multiple_frames(void) { return ok ? 0 : 1; } +/* Stereo-output плееров также принимает mono Opus; upmix выполняет сам libopus. */ +static int test_mono_packet_stereo_output(void) { + printf("test_mono_packet_stereo_output: "); + opus_codec_encoder_t* encoder = opus_codec_encoder_create(48000, 1); + opus_codec_decoder_t* decoder = opus_codec_decoder_create(48000, 2); + int ok = encoder && decoder; + if (ok) { + int16_t input[960], output[1920]; uint8_t packet[1275]; + for (unsigned i = 0; i < 960; i++) input[i] = (int16_t)(6000 * sin(i * 0.1)); + int length = opus_codec_encode(encoder, input, 960, packet, sizeof(packet)); + ok = length > 0 && opus_codec_decode(decoder, packet, length, output, 960) == 960; + int audible = 0; + if (ok) for (unsigned i = 0; i < 960; i++) { audible |= output[2 * i] != 0; if (output[2 * i] != output[2 * i + 1]) ok = 0; } + ok = ok && audible; + } + opus_codec_encoder_destroy(encoder); opus_codec_decoder_destroy(decoder); + printf("%s\n", ok ? "PASS" : "FAIL"); return ok ? 0 : 1; +} + int main(void) { int failed = 0; failed += test_encode_decode_silence(); @@ -220,7 +239,8 @@ int main(void) { failed += test_null_args(); failed += test_default_sample_rate(); failed += test_multiple_frames(); + failed += test_mono_packet_stereo_output(); - printf("\n%s: %d/%d tests passed\n", failed ? "FAIL" : "OK", 7 - failed, 7); + printf("\n%s: %d/%d tests passed\n", failed ? "FAIL" : "OK", 8 - failed, 8); return failed ? 1 : 0; }