mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2025-10-29 05:40:27 -04:00
Compare commits
19 commits
67cef2ba4a
...
e6898f2cb6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6898f2cb6 | ||
|
|
a813830024 | ||
|
|
a837dcd40b | ||
|
|
752de866ae | ||
|
|
ec11859a48 | ||
|
|
1096d63468 | ||
|
|
955c9ae837 | ||
|
|
3e0f4daf60 | ||
|
|
5d21e12658 | ||
|
|
f1ffd5e5e8 | ||
|
|
80e7302a05 | ||
|
|
b57bd00be0 | ||
|
|
c1e737bbe4 | ||
|
|
76a31a47c2 | ||
|
|
23c449af5d | ||
|
|
94d0d8bc09 | ||
|
|
0276bb5b06 | ||
|
|
614186a590 | ||
|
|
64096309fc |
20 changed files with 1080 additions and 307 deletions
|
|
@ -46,6 +46,7 @@ include:
|
||||||
bluez-libs-devel
|
bluez-libs-devel
|
||||||
clang
|
clang
|
||||||
dbus-devel
|
dbus-devel
|
||||||
|
desktop-file-utils
|
||||||
doxygen
|
doxygen
|
||||||
fdk-aac-free-devel
|
fdk-aac-free-devel
|
||||||
file
|
file
|
||||||
|
|
@ -113,6 +114,7 @@ include:
|
||||||
FDO_DISTRIBUTION_VERSION: '22.04'
|
FDO_DISTRIBUTION_VERSION: '22.04'
|
||||||
FDO_DISTRIBUTION_PACKAGES: >-
|
FDO_DISTRIBUTION_PACKAGES: >-
|
||||||
debhelper-compat
|
debhelper-compat
|
||||||
|
desktop-file-utils
|
||||||
findutils
|
findutils
|
||||||
git
|
git
|
||||||
libapparmor-dev
|
libapparmor-dev
|
||||||
|
|
@ -148,12 +150,14 @@ include:
|
||||||
.debian:
|
.debian:
|
||||||
variables:
|
variables:
|
||||||
# Update this tag when you want to trigger a rebuild
|
# Update this tag when you want to trigger a rebuild
|
||||||
BASE_TAG: '2025-08-10.0'
|
BASE_TAG: '2025-08-10.1'
|
||||||
FDO_DISTRIBUTION_VERSION: 'trixie'
|
FDO_DISTRIBUTION_VERSION: 'trixie'
|
||||||
FDO_DISTRIBUTION_PACKAGES: >-
|
FDO_DISTRIBUTION_PACKAGES: >-
|
||||||
build-essential
|
build-essential
|
||||||
|
desktop-file-utils
|
||||||
dpkg-dev
|
dpkg-dev
|
||||||
findutils
|
findutils
|
||||||
|
gettext
|
||||||
git
|
git
|
||||||
meson
|
meson
|
||||||
|
|
||||||
|
|
@ -182,6 +186,7 @@ include:
|
||||||
gcc
|
gcc
|
||||||
g++
|
g++
|
||||||
dbus-dev
|
dbus-dev
|
||||||
|
desktop-file-utils
|
||||||
doxygen
|
doxygen
|
||||||
elogind-dev
|
elogind-dev
|
||||||
eudev-dev
|
eudev-dev
|
||||||
|
|
|
||||||
341
po/tr.po
341
po/tr.po
|
|
@ -1,46 +1,51 @@
|
||||||
# Turkish translation for PipeWire.
|
# Turkish translation for PipeWire.
|
||||||
# Copyright (C) 2014-2024 PipeWire's COPYRIGHT HOLDER
|
# Copyright (C) 2014-2025 PipeWire's COPYRIGHT HOLDER
|
||||||
# This file is distributed under the same license as the PipeWire package.
|
# This file is distributed under the same license as the PipeWire package.
|
||||||
#
|
#
|
||||||
# Necdet Yücel <necdetyucel@gmail.com>, 2014.
|
# Necdet Yücel <necdetyucel@gmail.com>, 2014.
|
||||||
# Kaan Özdinçer <kaanozdincer@gmail.com>, 2014.
|
# Kaan Özdinçer <kaanozdincer@gmail.com>, 2014.
|
||||||
# Muhammet Kara <muhammetk@gmail.com>, 2015, 2016, 2017.
|
# Muhammet Kara <muhammetk@gmail.com>, 2015, 2016, 2017.
|
||||||
# Oğuz Ersen <oguz@ersen.moe>, 2021-2022.
|
# Oğuz Ersen <oguz@ersen.moe>, 2021-2022.
|
||||||
# Sabri Ünal <libreajans@gmail.com>, 2024.
|
# Sabri Ünal <yakushabb@gmail.com>, 2024, 2025.
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PipeWire master\n"
|
"Project-Id-Version: PipeWire master\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/"
|
||||||
"POT-Creation-Date: 2024-02-25 03:43+0300\n"
|
"issues\n"
|
||||||
"PO-Revision-Date: 2024-02-25 03:49+0300\n"
|
"POT-Creation-Date: 2025-10-24 15:37+0000\n"
|
||||||
"Last-Translator: Sabri Ünal <libreajans@gmail.com>\n"
|
"PO-Revision-Date: 2025-10-24 20:15+0300\n"
|
||||||
|
"Last-Translator: Sabri Ünal <yakushabb@gmail.com>\n"
|
||||||
"Language-Team: Türkçe <takim@gnome.org.tr>\n"
|
"Language-Team: Türkçe <takim@gnome.org.tr>\n"
|
||||||
"Language: tr\n"
|
"Language: tr\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
"X-Generator: Poedit 3.4.2\n"
|
"X-Generator: Poedit 3.8\n"
|
||||||
|
|
||||||
#: src/daemon/pipewire.c:26
|
#: src/daemon/pipewire.c:29
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"%s [options]\n"
|
"%s [options]\n"
|
||||||
" -h, --help Show this help\n"
|
" -h, --help Show this help\n"
|
||||||
|
" -v, --verbose Increase verbosity by one level\n"
|
||||||
" --version Show version\n"
|
" --version Show version\n"
|
||||||
" -c, --config Load config (Default %s)\n"
|
" -c, --config Load config (Default %s)\n"
|
||||||
|
" -P --properties Set context properties\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"%s [seçenekler]\n"
|
"%s [seçenekler]\n"
|
||||||
" -h, --help Bu yardımı göster\n"
|
" -h, --help Bu yardımı göster\n"
|
||||||
|
" -v, --verbose Ayrıntı düzeyini bir düzey artır\n"
|
||||||
" --version Sürümü göster\n"
|
" --version Sürümü göster\n"
|
||||||
" -c, --config Yapılandırmayı yükle (Öntanımlı %s)\n"
|
" -c, --config Yapılandırmayı yükle (Öntanımlı %s)\n"
|
||||||
|
" -P --properties Bağlam özelliklerini ayarla\n"
|
||||||
|
|
||||||
#: src/daemon/pipewire.desktop.in:4
|
#: src/daemon/pipewire.desktop.in:3
|
||||||
msgid "PipeWire Media System"
|
msgid "PipeWire Media System"
|
||||||
msgstr "PipeWire Ortam Sistemi"
|
msgstr "PipeWire Ortam Sistemi"
|
||||||
|
|
||||||
#: src/daemon/pipewire.desktop.in:5
|
#: src/daemon/pipewire.desktop.in:4
|
||||||
msgid "Start the PipeWire Media System"
|
msgid "Start the PipeWire Media System"
|
||||||
msgstr "PipeWire Ortam Sistemini Başlat"
|
msgstr "PipeWire Ortam Sistemini Başlat"
|
||||||
|
|
||||||
|
|
@ -54,26 +59,26 @@ msgstr "%s%s%s tüneli"
|
||||||
msgid "Dummy Output"
|
msgid "Dummy Output"
|
||||||
msgstr "Temsili Çıkış"
|
msgstr "Temsili Çıkış"
|
||||||
|
|
||||||
#: src/modules/module-pulse-tunnel.c:774
|
#: src/modules/module-pulse-tunnel.c:760
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "Tunnel for %s@%s"
|
msgid "Tunnel for %s@%s"
|
||||||
msgstr "%s@%s için tünel"
|
msgstr "%s@%s için tünel"
|
||||||
|
|
||||||
#: src/modules/module-zeroconf-discover.c:315
|
#: src/modules/module-zeroconf-discover.c:320
|
||||||
msgid "Unknown device"
|
msgid "Unknown device"
|
||||||
msgstr "Bilinmeyen aygıt"
|
msgstr "Bilinmeyen aygıt"
|
||||||
|
|
||||||
#: src/modules/module-zeroconf-discover.c:327
|
#: src/modules/module-zeroconf-discover.c:332
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "%s on %s@%s"
|
msgid "%s on %s@%s"
|
||||||
msgstr "%s, %s@%s"
|
msgstr "%s, %s@%s"
|
||||||
|
|
||||||
#: src/modules/module-zeroconf-discover.c:331
|
#: src/modules/module-zeroconf-discover.c:336
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "%s on %s"
|
msgid "%s on %s"
|
||||||
msgstr "%s, %s"
|
msgstr "%s, %s"
|
||||||
|
|
||||||
#: src/tools/pw-cat.c:991
|
#: src/tools/pw-cat.c:1096
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"%s [options] [<file>|-]\n"
|
"%s [options] [<file>|-]\n"
|
||||||
|
|
@ -88,7 +93,7 @@ msgstr ""
|
||||||
" -v, --verbose Ayrıntılı işlemleri etkinleştir\n"
|
" -v, --verbose Ayrıntılı işlemleri etkinleştir\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
|
||||||
#: src/tools/pw-cat.c:998
|
#: src/tools/pw-cat.c:1103
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid ""
|
msgid ""
|
||||||
" -R, --remote Remote daemon name\n"
|
" -R, --remote Remote daemon name\n"
|
||||||
|
|
@ -122,7 +127,7 @@ msgstr ""
|
||||||
" -P --properties Düğüm özelliklerini ayarla\n"
|
" -P --properties Düğüm özelliklerini ayarla\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
|
||||||
#: src/tools/pw-cat.c:1016
|
#: src/tools/pw-cat.c:1121
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid ""
|
msgid ""
|
||||||
" --rate Sample rate (req. for rec) (default "
|
" --rate Sample rate (req. for rec) (default "
|
||||||
|
|
@ -139,6 +144,10 @@ msgid ""
|
||||||
" --volume Stream volume 0-1.0 (default %.3f)\n"
|
" --volume Stream volume 0-1.0 (default %.3f)\n"
|
||||||
" -q --quality Resampler quality (0 - 15) (default "
|
" -q --quality Resampler quality (0 - 15) (default "
|
||||||
"%d)\n"
|
"%d)\n"
|
||||||
|
" -a, --raw RAW mode\n"
|
||||||
|
" -M, --force-midi Force midi format, one of \"midi\" "
|
||||||
|
"or \"ump\", (default ump)\n"
|
||||||
|
" -n, --sample-count COUNT Stop after COUNT samples\n"
|
||||||
"\n"
|
"\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
" --rate Örnekleme oranı (kayıt için gerekli) "
|
" --rate Örnekleme oranı (kayıt için gerekli) "
|
||||||
|
|
@ -156,15 +165,21 @@ msgstr ""
|
||||||
"%.3f)\n"
|
"%.3f)\n"
|
||||||
" -q --quality Yeniden örnekleyici kalitesi (0 - "
|
" -q --quality Yeniden örnekleyici kalitesi (0 - "
|
||||||
"15) (öntanımlı %d)\n"
|
"15) (öntanımlı %d)\n"
|
||||||
|
" -a, --raw HAM kipi\n"
|
||||||
|
" -M, --force-midi Midi biçimini zorla, ikisinden "
|
||||||
|
"birisi \"midi\" ya da\"ump\", (öntanımlı ump)\n"
|
||||||
|
" -n, --sample-count COUNT COUNT örnekleme sonrası dur\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
|
||||||
#: src/tools/pw-cat.c:1033
|
#: src/tools/pw-cat.c:1141
|
||||||
msgid ""
|
msgid ""
|
||||||
" -p, --playback Playback mode\n"
|
" -p, --playback Playback mode\n"
|
||||||
" -r, --record Recording mode\n"
|
" -r, --record Recording mode\n"
|
||||||
" -m, --midi Midi mode\n"
|
" -m, --midi Midi mode\n"
|
||||||
" -d, --dsd DSD mode\n"
|
" -d, --dsd DSD mode\n"
|
||||||
" -o, --encoded Encoded mode\n"
|
" -o, --encoded Encoded mode\n"
|
||||||
|
" -s, --sysex SysEx mode\n"
|
||||||
|
" -c, --midi-clip MIDI clip mode\n"
|
||||||
"\n"
|
"\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
" -p, --playback Çalma kipi\n"
|
" -p, --playback Çalma kipi\n"
|
||||||
|
|
@ -172,9 +187,11 @@ msgstr ""
|
||||||
" -m, --midi Midi kipi\n"
|
" -m, --midi Midi kipi\n"
|
||||||
" -d, --dsd DSD kipi\n"
|
" -d, --dsd DSD kipi\n"
|
||||||
" -o, --encoded Kodlanmış kip\n"
|
" -o, --encoded Kodlanmış kip\n"
|
||||||
|
" -s, --sysex SysEx kipi\n"
|
||||||
|
" -c, --midi-clip MIDI klip kipi\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
|
||||||
#: src/tools/pw-cli.c:2252
|
#: src/tools/pw-cli.c:2386
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"%s [options] [command]\n"
|
"%s [options] [command]\n"
|
||||||
|
|
@ -193,195 +210,203 @@ msgstr ""
|
||||||
" -r, --remote Uzak arka plan programı adı\n"
|
" -r, --remote Uzak arka plan programı adı\n"
|
||||||
" -m, --monitor Etkinliği izle\n"
|
" -m, --monitor Etkinliği izle\n"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/acp.c:327
|
#: spa/plugins/alsa/acp/acp.c:361
|
||||||
msgid "Pro Audio"
|
msgid "Pro Audio"
|
||||||
msgstr "Profesyonel Ses"
|
msgstr "Profesyonel Ses"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/acp.c:488 spa/plugins/alsa/acp/alsa-mixer.c:4633
|
#: spa/plugins/alsa/acp/acp.c:537 spa/plugins/alsa/acp/alsa-mixer.c:4699
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1701
|
#: spa/plugins/bluez5/bluez5-device.c:1976
|
||||||
msgid "Off"
|
msgid "Off"
|
||||||
msgstr "Kapalı"
|
msgstr "Kapalı"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2652
|
#: spa/plugins/alsa/acp/acp.c:620
|
||||||
|
#, c-format
|
||||||
|
msgid "%s [ALSA UCM error]"
|
||||||
|
msgstr "%s [ALSA UCM hatası]"
|
||||||
|
|
||||||
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2721
|
||||||
msgid "Input"
|
msgid "Input"
|
||||||
msgstr "Giriş"
|
msgstr "Giriş"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2653
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2722
|
||||||
msgid "Docking Station Input"
|
msgid "Docking Station Input"
|
||||||
msgstr "Yerleştirme İstasyonu Girişi"
|
msgstr "Yerleştirme İstasyonu Girişi"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2654
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2723
|
||||||
msgid "Docking Station Microphone"
|
msgid "Docking Station Microphone"
|
||||||
msgstr "Yerleştirme İstasyonu Mikrofonu"
|
msgstr "Yerleştirme İstasyonu Mikrofonu"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2655
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2724
|
||||||
msgid "Docking Station Line In"
|
msgid "Docking Station Line In"
|
||||||
msgstr "Yerleştirme İstasyonu Hat Girişi"
|
msgstr "Yerleştirme İstasyonu Hat Girişi"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2656
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2725
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2747
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2816
|
||||||
msgid "Line In"
|
msgid "Line In"
|
||||||
msgstr "Hat Girişi"
|
msgstr "Hat Girişi"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2657
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2726
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2741
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2810
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1989
|
#: spa/plugins/bluez5/bluez5-device.c:2374
|
||||||
msgid "Microphone"
|
msgid "Microphone"
|
||||||
msgstr "Mikrofon"
|
msgstr "Mikrofon"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2658
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2727
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2742
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2811
|
||||||
msgid "Front Microphone"
|
msgid "Front Microphone"
|
||||||
msgstr "Ön Mikrofon"
|
msgstr "Ön Mikrofon"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2659
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2728
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2743
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2812
|
||||||
msgid "Rear Microphone"
|
msgid "Rear Microphone"
|
||||||
msgstr "Arka Mikrofon"
|
msgstr "Arka Mikrofon"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2660
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2729
|
||||||
msgid "External Microphone"
|
msgid "External Microphone"
|
||||||
msgstr "Harici Mikrofon"
|
msgstr "Harici Mikrofon"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2661
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2730
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2745
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2814
|
||||||
msgid "Internal Microphone"
|
msgid "Internal Microphone"
|
||||||
msgstr "Dahili Mikrofon"
|
msgstr "Dahili Mikrofon"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2662
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2731
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2748
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2817
|
||||||
msgid "Radio"
|
msgid "Radio"
|
||||||
msgstr "Radyo"
|
msgstr "Radyo"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2663
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2732
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2749
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2818
|
||||||
msgid "Video"
|
msgid "Video"
|
||||||
msgstr "Video"
|
msgstr "Video"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2664
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2733
|
||||||
msgid "Automatic Gain Control"
|
msgid "Automatic Gain Control"
|
||||||
msgstr "Otomatik Kazanç Denetimi"
|
msgstr "Otomatik Kazanç Denetimi"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2665
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2734
|
||||||
msgid "No Automatic Gain Control"
|
msgid "No Automatic Gain Control"
|
||||||
msgstr "Otomatik Kazanç Denetimi Yok"
|
msgstr "Otomatik Kazanç Denetimi Yok"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2666
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2735
|
||||||
msgid "Boost"
|
msgid "Boost"
|
||||||
msgstr "Artır"
|
msgstr "Artır"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2667
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2736
|
||||||
msgid "No Boost"
|
msgid "No Boost"
|
||||||
msgstr "Artırma Yok"
|
msgstr "Artırma Yok"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2668
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2737
|
||||||
msgid "Amplifier"
|
msgid "Amplifier"
|
||||||
msgstr "Yükseltici"
|
msgstr "Yükseltici"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2669
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2738
|
||||||
msgid "No Amplifier"
|
msgid "No Amplifier"
|
||||||
msgstr "Yükseltici Yok"
|
msgstr "Yükseltici Yok"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2670
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2739
|
||||||
msgid "Bass Boost"
|
msgid "Bass Boost"
|
||||||
msgstr "Bas Artır"
|
msgstr "Bas Artır"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2671
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2740
|
||||||
msgid "No Bass Boost"
|
msgid "No Bass Boost"
|
||||||
msgstr "Bas Artırma Yok"
|
msgstr "Bas Artırma Yok"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2672
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2741
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1995
|
#: spa/plugins/bluez5/bluez5-device.c:2380
|
||||||
msgid "Speaker"
|
msgid "Speaker"
|
||||||
msgstr "Hoparlör"
|
msgstr "Hoparlör"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2673
|
#. Don't call it "headset", the HF one has the mic
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2751
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2742
|
||||||
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2820
|
||||||
|
#: spa/plugins/bluez5/bluez5-device.c:2386
|
||||||
|
#: spa/plugins/bluez5/bluez5-device.c:2453
|
||||||
msgid "Headphones"
|
msgid "Headphones"
|
||||||
msgstr "Kulaklık"
|
msgstr "Kulaklık"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2740
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2809
|
||||||
msgid "Analog Input"
|
msgid "Analog Input"
|
||||||
msgstr "Analog Giriş"
|
msgstr "Analog Giriş"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2744
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2813
|
||||||
msgid "Dock Microphone"
|
msgid "Dock Microphone"
|
||||||
msgstr "Yapışık Mikrofon"
|
msgstr "Yapışık Mikrofon"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2746
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2815
|
||||||
msgid "Headset Microphone"
|
msgid "Headset Microphone"
|
||||||
msgstr "Mikrofonlu Kulaklık"
|
msgstr "Mikrofonlu Kulaklık"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2750
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2819
|
||||||
msgid "Analog Output"
|
msgid "Analog Output"
|
||||||
msgstr "Analog Çıkış"
|
msgstr "Analog Çıkış"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2752
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2821
|
||||||
msgid "Headphones 2"
|
msgid "Headphones 2"
|
||||||
msgstr "Kulaklık 2"
|
msgstr "Kulaklık 2"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2753
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2822
|
||||||
msgid "Headphones Mono Output"
|
msgid "Headphones Mono Output"
|
||||||
msgstr "Kulaklık Tek Kanallı Çıkış"
|
msgstr "Kulaklık Tek Kanallı Çıkış"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2754
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2823
|
||||||
msgid "Line Out"
|
msgid "Line Out"
|
||||||
msgstr "Hat Çıkışı"
|
msgstr "Hat Çıkışı"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2755
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2824
|
||||||
msgid "Analog Mono Output"
|
msgid "Analog Mono Output"
|
||||||
msgstr "Analog Tek Kanallı Çıkış"
|
msgstr "Analog Tek Kanallı Çıkış"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2756
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2825
|
||||||
msgid "Speakers"
|
msgid "Speakers"
|
||||||
msgstr "Hoparlörler"
|
msgstr "Hoparlörler"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2757
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2826
|
||||||
msgid "HDMI / DisplayPort"
|
msgid "HDMI / DisplayPort"
|
||||||
msgstr "HDMI / DisplayPort"
|
msgstr "HDMI / DisplayPort"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2758
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2827
|
||||||
msgid "Digital Output (S/PDIF)"
|
msgid "Digital Output (S/PDIF)"
|
||||||
msgstr "Sayısal Çıkış (S/PDIF)"
|
msgstr "Sayısal Çıkış (S/PDIF)"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2759
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2828
|
||||||
msgid "Digital Input (S/PDIF)"
|
msgid "Digital Input (S/PDIF)"
|
||||||
msgstr "Sayısal Giriş (S/PDIF)"
|
msgstr "Sayısal Giriş (S/PDIF)"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2760
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2829
|
||||||
msgid "Multichannel Input"
|
msgid "Multichannel Input"
|
||||||
msgstr "Çok Kanallı Giriş"
|
msgstr "Çok Kanallı Giriş"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2761
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2830
|
||||||
msgid "Multichannel Output"
|
msgid "Multichannel Output"
|
||||||
msgstr "Çok Kanallı Çıkış"
|
msgstr "Çok Kanallı Çıkış"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2762
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2831
|
||||||
msgid "Game Output"
|
msgid "Game Output"
|
||||||
msgstr "Oyun Çıkışı"
|
msgstr "Oyun Çıkışı"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2763
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2832
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2764
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2833
|
||||||
msgid "Chat Output"
|
msgid "Chat Output"
|
||||||
msgstr "Sohbet Çıkışı"
|
msgstr "Sohbet Çıkışı"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2765
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2834
|
||||||
msgid "Chat Input"
|
msgid "Chat Input"
|
||||||
msgstr "Sohbet Girişi"
|
msgstr "Sohbet Girişi"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:2766
|
#: spa/plugins/alsa/acp/alsa-mixer.c:2835
|
||||||
msgid "Virtual Surround 7.1"
|
msgid "Virtual Surround 7.1"
|
||||||
msgstr "Sanal Çevresel Ses 7.1"
|
msgstr "Sanal Çevresel Ses 7.1"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4456
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4522
|
||||||
msgid "Analog Mono"
|
msgid "Analog Mono"
|
||||||
msgstr "Analog Tek Kanallı"
|
msgstr "Analog Tek Kanallı"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4457
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4523
|
||||||
msgid "Analog Mono (Left)"
|
msgid "Analog Mono (Left)"
|
||||||
msgstr "Analog Tek Kanallı (Sol)"
|
msgstr "Analog Tek Kanallı (Sol)"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4458
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4524
|
||||||
msgid "Analog Mono (Right)"
|
msgid "Analog Mono (Right)"
|
||||||
msgstr "Analog Tek Kanallı (Sağ)"
|
msgstr "Analog Tek Kanallı (Sağ)"
|
||||||
|
|
||||||
|
|
@ -390,147 +415,147 @@ msgstr "Analog Tek Kanallı (Sağ)"
|
||||||
#. * here would lead to the source name to become "Analog Stereo Input
|
#. * here would lead to the source name to become "Analog Stereo Input
|
||||||
#. * Input". The same logic applies to analog-stereo-output,
|
#. * Input". The same logic applies to analog-stereo-output,
|
||||||
#. * multichannel-input and multichannel-output.
|
#. * multichannel-input and multichannel-output.
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4459
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4525
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4467
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4533
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4468
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4534
|
||||||
msgid "Analog Stereo"
|
msgid "Analog Stereo"
|
||||||
msgstr "Analog Stereo"
|
msgstr "Analog Stereo"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4460
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4526
|
||||||
msgid "Mono"
|
msgid "Mono"
|
||||||
msgstr "Tek Kanallı"
|
msgstr "Tek Kanallı"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4461
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4527
|
||||||
msgid "Stereo"
|
msgid "Stereo"
|
||||||
msgstr "Stereo"
|
msgstr "Stereo"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4469
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4535
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4627
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4693
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1977
|
#: spa/plugins/bluez5/bluez5-device.c:2362
|
||||||
msgid "Headset"
|
msgid "Headset"
|
||||||
msgstr "Kulaklık"
|
msgstr "Kulaklık"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4470
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4536
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4628
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4694
|
||||||
msgid "Speakerphone"
|
msgid "Speakerphone"
|
||||||
msgstr "Hoparlör"
|
msgstr "Hoparlör"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4471
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4537
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4472
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4538
|
||||||
msgid "Multichannel"
|
msgid "Multichannel"
|
||||||
msgstr "Çok kanallı"
|
msgstr "Çok kanallı"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4473
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4539
|
||||||
msgid "Analog Surround 2.1"
|
msgid "Analog Surround 2.1"
|
||||||
msgstr "Analog Çevresel Ses 2.1"
|
msgstr "Analog Çevresel Ses 2.1"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4474
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4540
|
||||||
msgid "Analog Surround 3.0"
|
msgid "Analog Surround 3.0"
|
||||||
msgstr "Analog Çevresel Ses 3.0"
|
msgstr "Analog Çevresel Ses 3.0"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4475
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4541
|
||||||
msgid "Analog Surround 3.1"
|
msgid "Analog Surround 3.1"
|
||||||
msgstr "Analog Çevresel Ses 3.1"
|
msgstr "Analog Çevresel Ses 3.1"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4476
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4542
|
||||||
msgid "Analog Surround 4.0"
|
msgid "Analog Surround 4.0"
|
||||||
msgstr "Analog Çevresel Ses 4.0"
|
msgstr "Analog Çevresel Ses 4.0"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4477
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4543
|
||||||
msgid "Analog Surround 4.1"
|
msgid "Analog Surround 4.1"
|
||||||
msgstr "Analog Çevresel Ses 4.1"
|
msgstr "Analog Çevresel Ses 4.1"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4478
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4544
|
||||||
msgid "Analog Surround 5.0"
|
msgid "Analog Surround 5.0"
|
||||||
msgstr "Analog Çevresel Ses 5.0"
|
msgstr "Analog Çevresel Ses 5.0"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4479
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4545
|
||||||
msgid "Analog Surround 5.1"
|
msgid "Analog Surround 5.1"
|
||||||
msgstr "Analog Çevresel Ses 5.1"
|
msgstr "Analog Çevresel Ses 5.1"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4480
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4546
|
||||||
msgid "Analog Surround 6.0"
|
msgid "Analog Surround 6.0"
|
||||||
msgstr "Analog Çevresel Ses 6.0"
|
msgstr "Analog Çevresel Ses 6.0"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4481
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4547
|
||||||
msgid "Analog Surround 6.1"
|
msgid "Analog Surround 6.1"
|
||||||
msgstr "Analog Çevresel Ses 6.1"
|
msgstr "Analog Çevresel Ses 6.1"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4482
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4548
|
||||||
msgid "Analog Surround 7.0"
|
msgid "Analog Surround 7.0"
|
||||||
msgstr "Analog Çevresel Ses 7.0"
|
msgstr "Analog Çevresel Ses 7.0"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4483
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4549
|
||||||
msgid "Analog Surround 7.1"
|
msgid "Analog Surround 7.1"
|
||||||
msgstr "Analog Çevresel Ses 7.1"
|
msgstr "Analog Çevresel Ses 7.1"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4484
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4550
|
||||||
msgid "Digital Stereo (IEC958)"
|
msgid "Digital Stereo (IEC958)"
|
||||||
msgstr "Sayısal Stereo (IEC958)"
|
msgstr "Sayısal Stereo (IEC958)"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4485
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4551
|
||||||
msgid "Digital Surround 4.0 (IEC958/AC3)"
|
msgid "Digital Surround 4.0 (IEC958/AC3)"
|
||||||
msgstr "Sayısal Çevresel Ses 4.0 (IEC958/AC3)"
|
msgstr "Sayısal Çevresel Ses 4.0 (IEC958/AC3)"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4486
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4552
|
||||||
msgid "Digital Surround 5.1 (IEC958/AC3)"
|
msgid "Digital Surround 5.1 (IEC958/AC3)"
|
||||||
msgstr "Sayısal Çevresel Ses 5.1 (IEC958/AC3)"
|
msgstr "Sayısal Çevresel Ses 5.1 (IEC958/AC3)"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4487
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4553
|
||||||
msgid "Digital Surround 5.1 (IEC958/DTS)"
|
msgid "Digital Surround 5.1 (IEC958/DTS)"
|
||||||
msgstr "Sayısal Çevresel Ses 5.1 (IEC958/DTS)"
|
msgstr "Sayısal Çevresel Ses 5.1 (IEC958/DTS)"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4488
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4554
|
||||||
msgid "Digital Stereo (HDMI)"
|
msgid "Digital Stereo (HDMI)"
|
||||||
msgstr "Sayısal Stereo (HDMI)"
|
msgstr "Sayısal Stereo (HDMI)"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4489
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4555
|
||||||
msgid "Digital Surround 5.1 (HDMI)"
|
msgid "Digital Surround 5.1 (HDMI)"
|
||||||
msgstr "Sayısal Çevresel Ses 5.1 (HDMI)"
|
msgstr "Sayısal Çevresel Ses 5.1 (HDMI)"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4490
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4556
|
||||||
msgid "Chat"
|
msgid "Chat"
|
||||||
msgstr "Sohbet"
|
msgstr "Sohbet"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4491
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4557
|
||||||
msgid "Game"
|
msgid "Game"
|
||||||
msgstr "Oyun"
|
msgstr "Oyun"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4625
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4691
|
||||||
msgid "Analog Mono Duplex"
|
msgid "Analog Mono Duplex"
|
||||||
msgstr "Analog Tek Kanallı İkili"
|
msgstr "Analog Tek Kanallı İkili"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4626
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4692
|
||||||
msgid "Analog Stereo Duplex"
|
msgid "Analog Stereo Duplex"
|
||||||
msgstr "Analog İkili Stereo"
|
msgstr "Analog İkili Stereo"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4629
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4695
|
||||||
msgid "Digital Stereo Duplex (IEC958)"
|
msgid "Digital Stereo Duplex (IEC958)"
|
||||||
msgstr "Sayısal İkili Stereo (IEC958)"
|
msgstr "Sayısal İkili Stereo (IEC958)"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4630
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4696
|
||||||
msgid "Multichannel Duplex"
|
msgid "Multichannel Duplex"
|
||||||
msgstr "Çok Kanallı İkili"
|
msgstr "Çok Kanallı İkili"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4631
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4697
|
||||||
msgid "Stereo Duplex"
|
msgid "Stereo Duplex"
|
||||||
msgstr "İkili Stereo"
|
msgstr "İkili Stereo"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4632
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4698
|
||||||
msgid "Mono Chat + 7.1 Surround"
|
msgid "Mono Chat + 7.1 Surround"
|
||||||
msgstr "Tek Kanallı Sohbet + 7.1 Çevresel Ses"
|
msgstr "Tek Kanallı Sohbet + 7.1 Çevresel Ses"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4733
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4799
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "%s Output"
|
msgid "%s Output"
|
||||||
msgstr "%s Çıkışı"
|
msgstr "%s Çıkışı"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-mixer.c:4741
|
#: spa/plugins/alsa/acp/alsa-mixer.c:4807
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "%s Input"
|
msgid "%s Input"
|
||||||
msgstr "%s Girişi"
|
msgstr "%s Girişi"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-util.c:1220 spa/plugins/alsa/acp/alsa-util.c:1314
|
#: spa/plugins/alsa/acp/alsa-util.c:1233 spa/plugins/alsa/acp/alsa-util.c:1327
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
|
"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu "
|
||||||
|
|
@ -547,16 +572,16 @@ msgstr[0] ""
|
||||||
"Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA "
|
"Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA "
|
||||||
"geliştiricilerine bildirin."
|
"geliştiricilerine bildirin."
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-util.c:1286
|
#: spa/plugins/alsa/acp/alsa-util.c:1299
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s"
|
"snd_pcm_delay() returned a value that is exceptionally large: %li byte "
|
||||||
"%lu ms).\n"
|
"(%s%lu ms).\n"
|
||||||
"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
|
"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
|
||||||
"to the ALSA developers."
|
"to the ALSA developers."
|
||||||
msgid_plural ""
|
msgid_plural ""
|
||||||
"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s"
|
"snd_pcm_delay() returned a value that is exceptionally large: %li bytes "
|
||||||
"%lu ms).\n"
|
"(%s%lu ms).\n"
|
||||||
"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
|
"Most likely this is a bug in the ALSA driver '%s'. Please report this issue "
|
||||||
"to the ALSA developers."
|
"to the ALSA developers."
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
|
|
@ -564,7 +589,7 @@ msgstr[0] ""
|
||||||
"Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA "
|
"Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA "
|
||||||
"geliştiricilerine bildirin."
|
"geliştiricilerine bildirin."
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-util.c:1333
|
#: spa/plugins/alsa/acp/alsa-util.c:1346
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
|
"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail "
|
||||||
|
|
@ -577,7 +602,7 @@ msgstr ""
|
||||||
"Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA "
|
"Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA "
|
||||||
"geliştiricilerine bildirin."
|
"geliştiricilerine bildirin."
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/alsa-util.c:1376
|
#: spa/plugins/alsa/acp/alsa-util.c:1389
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
|
"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte "
|
||||||
|
|
@ -595,112 +620,112 @@ msgstr[0] ""
|
||||||
"Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA "
|
"Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA "
|
||||||
"geliştiricilerine bildirin."
|
"geliştiricilerine bildirin."
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/channelmap.h:457
|
#: spa/plugins/alsa/acp/channelmap.h:460
|
||||||
msgid "(invalid)"
|
msgid "(invalid)"
|
||||||
msgstr "(geçersiz)"
|
msgstr "(geçersiz)"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/compat.c:193
|
#: spa/plugins/alsa/acp/compat.c:194
|
||||||
msgid "Built-in Audio"
|
msgid "Built-in Audio"
|
||||||
msgstr "Dahili Ses"
|
msgstr "Dahili Ses"
|
||||||
|
|
||||||
#: spa/plugins/alsa/acp/compat.c:198
|
#: spa/plugins/alsa/acp/compat.c:199
|
||||||
msgid "Modem"
|
msgid "Modem"
|
||||||
msgstr "Modem"
|
msgstr "Modem"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1712
|
#: spa/plugins/bluez5/bluez5-device.c:1987
|
||||||
msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
|
msgid "Audio Gateway (A2DP Source & HSP/HFP AG)"
|
||||||
msgstr "Ses Geçidi (A2DP Kaynak & HSP/HFP AG)"
|
msgstr "Ses Geçidi (A2DP Kaynak & HSP/HFP AG)"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1760
|
#: spa/plugins/bluez5/bluez5-device.c:2016
|
||||||
|
msgid "Audio Streaming for Hearing Aids (ASHA Sink)"
|
||||||
|
msgstr "İşitme Aygıtları İçin Ses Akışı (ASHA Alıcı)"
|
||||||
|
|
||||||
|
#: spa/plugins/bluez5/bluez5-device.c:2059
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "High Fidelity Playback (A2DP Sink, codec %s)"
|
msgid "High Fidelity Playback (A2DP Sink, codec %s)"
|
||||||
msgstr "Yüksek Kaliteli Çalma (A2DP Alıcı, çözücü %s)"
|
msgstr "Yüksek Kaliteli Çalma (A2DP Alıcı, çözücü %s)"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1763
|
#: spa/plugins/bluez5/bluez5-device.c:2062
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
|
msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)"
|
||||||
msgstr "Yüksek Kaliteli İkili (A2DP Kaynak/Alıcı, çözücü %s)"
|
msgstr "Yüksek Kaliteli İkili (A2DP Kaynak/Alıcı, çözücü %s)"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1771
|
#: spa/plugins/bluez5/bluez5-device.c:2070
|
||||||
msgid "High Fidelity Playback (A2DP Sink)"
|
msgid "High Fidelity Playback (A2DP Sink)"
|
||||||
msgstr "Yüksek Kaliteli Çalma (A2DP Alıcı)"
|
msgstr "Yüksek Kaliteli Çalma (A2DP Alıcı)"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1773
|
#: spa/plugins/bluez5/bluez5-device.c:2072
|
||||||
msgid "High Fidelity Duplex (A2DP Source/Sink)"
|
msgid "High Fidelity Duplex (A2DP Source/Sink)"
|
||||||
msgstr "Yüksek Kaliteli İkili (A2DP Kaynak/Alıcı)"
|
msgstr "Yüksek Kaliteli İkili (A2DP Kaynak/Alıcı)"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1823
|
#: spa/plugins/bluez5/bluez5-device.c:2146
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "High Fidelity Playback (BAP Sink, codec %s)"
|
msgid "High Fidelity Playback (BAP Sink, codec %s)"
|
||||||
msgstr "Yüksek Kaliteli Çalma (BAP Alıcı, çözücü %s)"
|
msgstr "Yüksek Kaliteli Çalma (BAP Alıcı, çözücü %s)"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1828
|
#: spa/plugins/bluez5/bluez5-device.c:2151
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "High Fidelity Input (BAP Source, codec %s)"
|
msgid "High Fidelity Input (BAP Source, codec %s)"
|
||||||
msgstr "Yüksek Kaliteli Giriş (BAP Kaynak, çözücü %s)"
|
msgstr "Yüksek Kaliteli Giriş (BAP Kaynak, çözücü %s)"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1832
|
#: spa/plugins/bluez5/bluez5-device.c:2155
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
|
msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)"
|
||||||
msgstr "Yüksek Kaliteli İkili (BAP Kaynak/Alıcı, çözücü %s)"
|
msgstr "Yüksek Kaliteli İkili (BAP Kaynak/Alıcı, çözücü %s)"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1841
|
#: spa/plugins/bluez5/bluez5-device.c:2164
|
||||||
msgid "High Fidelity Playback (BAP Sink)"
|
msgid "High Fidelity Playback (BAP Sink)"
|
||||||
msgstr "Yüksek Kaliteli Çalma (BAP Alıcı)"
|
msgstr "Yüksek Kaliteli Çalma (BAP Alıcı)"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1845
|
#: spa/plugins/bluez5/bluez5-device.c:2168
|
||||||
msgid "High Fidelity Input (BAP Source)"
|
msgid "High Fidelity Input (BAP Source)"
|
||||||
msgstr "Yüksek Kaliteli Giriş (BAP Kaynak)"
|
msgstr "Yüksek Kaliteli Giriş (BAP Kaynak)"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1848
|
#: spa/plugins/bluez5/bluez5-device.c:2171
|
||||||
msgid "High Fidelity Duplex (BAP Source/Sink)"
|
msgid "High Fidelity Duplex (BAP Source/Sink)"
|
||||||
msgstr "Yüksek Kaliteli İkili (BAP Kaynak/Alıcı)"
|
msgstr "Yüksek Kaliteli İkili (BAP Kaynak/Alıcı)"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1897
|
#: spa/plugins/bluez5/bluez5-device.c:2211
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "Headset Head Unit (HSP/HFP, codec %s)"
|
msgid "Headset Head Unit (HSP/HFP, codec %s)"
|
||||||
msgstr "Kulaklık Ana Birimi (HSP/HFP, çözücü %s)"
|
msgstr "Kulaklık Ana Birimi (HSP/HFP, çözücü %s)"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1978
|
#: spa/plugins/bluez5/bluez5-device.c:2363
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1983
|
#: spa/plugins/bluez5/bluez5-device.c:2368
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1990
|
#: spa/plugins/bluez5/bluez5-device.c:2375
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1996
|
#: spa/plugins/bluez5/bluez5-device.c:2381
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:2002
|
#: spa/plugins/bluez5/bluez5-device.c:2387
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:2008
|
#: spa/plugins/bluez5/bluez5-device.c:2393
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:2014
|
#: spa/plugins/bluez5/bluez5-device.c:2399
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:2020
|
#: spa/plugins/bluez5/bluez5-device.c:2405
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:2026
|
#: spa/plugins/bluez5/bluez5-device.c:2411
|
||||||
msgid "Handsfree"
|
msgid "Handsfree"
|
||||||
msgstr "Ahizesiz"
|
msgstr "Ahizesiz"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:1984
|
#: spa/plugins/bluez5/bluez5-device.c:2369
|
||||||
msgid "Handsfree (HFP)"
|
msgid "Handsfree (HFP)"
|
||||||
msgstr "Ahizesiz (HFP)"
|
msgstr "Ahizesiz (HFP)"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:2001
|
#: spa/plugins/bluez5/bluez5-device.c:2392
|
||||||
msgid "Headphone"
|
|
||||||
msgstr "Kulaklık"
|
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:2007
|
|
||||||
msgid "Portable"
|
msgid "Portable"
|
||||||
msgstr "Taşınabilir"
|
msgstr "Taşınabilir"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:2013
|
#: spa/plugins/bluez5/bluez5-device.c:2398
|
||||||
msgid "Car"
|
msgid "Car"
|
||||||
msgstr "Araba"
|
msgstr "Araba"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:2019
|
#: spa/plugins/bluez5/bluez5-device.c:2404
|
||||||
msgid "HiFi"
|
msgid "HiFi"
|
||||||
msgstr "Yüksek Kalite"
|
msgstr "Yüksek Kalite"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:2025
|
#: spa/plugins/bluez5/bluez5-device.c:2410
|
||||||
msgid "Phone"
|
msgid "Phone"
|
||||||
msgstr "Telefon"
|
msgstr "Telefon"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:2032
|
#: spa/plugins/bluez5/bluez5-device.c:2417
|
||||||
msgid "Bluetooth"
|
msgid "Bluetooth"
|
||||||
msgstr "Bluetooth"
|
msgstr "Bluetooth"
|
||||||
|
|
||||||
#: spa/plugins/bluez5/bluez5-device.c:2033
|
#: spa/plugins/bluez5/bluez5-device.c:2418
|
||||||
msgid "Bluetooth (HFP)"
|
msgid "Bluetooth (HFP)"
|
||||||
msgstr "Bluetooth (HFP)"
|
msgstr "Bluetooth (HFP)"
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,9 @@ static const struct spa_type_info spa_type_props[] = {
|
||||||
{ SPA_PROP_quality, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "quality", NULL },
|
{ SPA_PROP_quality, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "quality", NULL },
|
||||||
{ SPA_PROP_bluetoothAudioCodec, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "bluetoothAudioCodec", spa_type_bluetooth_audio_codec },
|
{ SPA_PROP_bluetoothAudioCodec, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "bluetoothAudioCodec", spa_type_bluetooth_audio_codec },
|
||||||
{ SPA_PROP_bluetoothOffloadActive, SPA_TYPE_Bool, SPA_TYPE_INFO_PROPS_BASE "bluetoothOffloadActive", NULL },
|
{ SPA_PROP_bluetoothOffloadActive, SPA_TYPE_Bool, SPA_TYPE_INFO_PROPS_BASE "bluetoothOffloadActive", NULL },
|
||||||
|
{ SPA_PROP_clockId, SPA_TYPE_String, SPA_TYPE_INFO_PROPS_BASE "clockId", NULL },
|
||||||
|
{ SPA_PROP_clockDevice, SPA_TYPE_String, SPA_TYPE_INFO_PROPS_BASE "clockDevice", NULL },
|
||||||
|
{ SPA_PROP_clockInterface, SPA_TYPE_String, SPA_TYPE_INFO_PROPS_BASE "clockInterface", NULL },
|
||||||
|
|
||||||
{ SPA_PROP_waveType, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "waveType", NULL },
|
{ SPA_PROP_waveType, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "waveType", NULL },
|
||||||
{ SPA_PROP_frequency, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "frequency", NULL },
|
{ SPA_PROP_frequency, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "frequency", NULL },
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,9 @@ enum spa_prop {
|
||||||
SPA_PROP_quality,
|
SPA_PROP_quality,
|
||||||
SPA_PROP_bluetoothAudioCodec,
|
SPA_PROP_bluetoothAudioCodec,
|
||||||
SPA_PROP_bluetoothOffloadActive,
|
SPA_PROP_bluetoothOffloadActive,
|
||||||
|
SPA_PROP_clockId,
|
||||||
|
SPA_PROP_clockDevice,
|
||||||
|
SPA_PROP_clockInterface,
|
||||||
|
|
||||||
SPA_PROP_START_Audio = 0x10000, /**< audio related properties */
|
SPA_PROP_START_Audio = 0x10000, /**< audio related properties */
|
||||||
SPA_PROP_waveType,
|
SPA_PROP_waveType,
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,15 @@ SPA_API_JSON void spa_json_init(struct spa_json * iter, const char *data, size_t
|
||||||
{
|
{
|
||||||
*iter = SPA_JSON_INIT(data, size);
|
*iter = SPA_JSON_INIT(data, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define SPA_JSON_INIT_RELAX(type,data,size) \
|
||||||
|
((struct spa_json) { (data), (data)+(size), NULL, (uint32_t)((type) == '[' ? 0x10 : 0x0), 0 })
|
||||||
|
|
||||||
|
SPA_API_JSON void spa_json_init_relax(struct spa_json * iter, char type, const char *data, size_t size)
|
||||||
|
{
|
||||||
|
*iter = SPA_JSON_INIT_RELAX(type, data, size);
|
||||||
|
}
|
||||||
|
|
||||||
#define SPA_JSON_ENTER(iter) ((struct spa_json) { (iter)->cur, (iter)->end, (iter), (iter)->state & 0xff0, 0 })
|
#define SPA_JSON_ENTER(iter) ((struct spa_json) { (iter)->cur, (iter)->end, (iter), (iter)->state & 0xff0, 0 })
|
||||||
|
|
||||||
SPA_API_JSON void spa_json_enter(struct spa_json * iter, struct spa_json * sub)
|
SPA_API_JSON void spa_json_enter(struct spa_json * iter, struct spa_json * sub)
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ SPA_API_JSON_UTILS int spa_json_begin_container(struct spa_json * iter,
|
||||||
spa_json_init(iter, data, size);
|
spa_json_init(iter, data, size);
|
||||||
res = spa_json_enter_container(iter, iter, type);
|
res = spa_json_enter_container(iter, iter, type);
|
||||||
if (res == -EPROTO && relax)
|
if (res == -EPROTO && relax)
|
||||||
spa_json_init(iter, data, size);
|
spa_json_init_relax(iter, type, data, size);
|
||||||
else if (res <= 0)
|
else if (res <= 0)
|
||||||
return res;
|
return res;
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
|
|
@ -931,6 +931,7 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case SPA_IO_Position:
|
case SPA_IO_Position:
|
||||||
this->io_position = data;
|
this->io_position = data;
|
||||||
|
this->recheck_format = true;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@
|
||||||
#include <spa/node/io.h>
|
#include <spa/node/io.h>
|
||||||
#include <spa/node/utils.h>
|
#include <spa/node/utils.h>
|
||||||
#include <spa/param/param.h>
|
#include <spa/param/param.h>
|
||||||
|
#include <spa/pod/filter.h>
|
||||||
|
#include <spa/pod/parser.h>
|
||||||
|
|
||||||
SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.driver");
|
SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.driver");
|
||||||
|
|
||||||
|
|
@ -48,12 +50,16 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.driver");
|
||||||
#define BW_PERIOD (3 * SPA_NSEC_PER_SEC)
|
#define BW_PERIOD (3 * SPA_NSEC_PER_SEC)
|
||||||
#define MAX_ERROR_MS 1
|
#define MAX_ERROR_MS 1
|
||||||
|
|
||||||
|
#define CLOCK_NAME_MAX 64
|
||||||
|
|
||||||
struct props {
|
struct props {
|
||||||
bool freewheel;
|
bool freewheel;
|
||||||
char clock_name[64];
|
char clock_name[CLOCK_NAME_MAX];
|
||||||
clockid_t clock_id;
|
clockid_t clock_id;
|
||||||
uint32_t freewheel_wait;
|
uint32_t freewheel_wait;
|
||||||
float resync_ms;
|
float resync_ms;
|
||||||
|
char clock_device[CLOCK_NAME_MAX];
|
||||||
|
char clock_interface[CLOCK_NAME_MAX];
|
||||||
};
|
};
|
||||||
|
|
||||||
struct clock_offset {
|
struct clock_offset {
|
||||||
|
|
@ -73,7 +79,10 @@ struct impl {
|
||||||
|
|
||||||
uint64_t info_all;
|
uint64_t info_all;
|
||||||
struct spa_node_info info;
|
struct spa_node_info info;
|
||||||
struct spa_param_info params[1];
|
#define NODE_PropInfo 0
|
||||||
|
#define NODE_Props 1
|
||||||
|
#define N_NODE_PARAMS 2
|
||||||
|
struct spa_param_info params[N_NODE_PARAMS];
|
||||||
|
|
||||||
struct spa_hook_list hooks;
|
struct spa_hook_list hooks;
|
||||||
struct spa_callbacks callbacks;
|
struct spa_callbacks callbacks;
|
||||||
|
|
@ -99,13 +108,20 @@ struct impl {
|
||||||
struct clock_offset nsec_offset;
|
struct clock_offset nsec_offset;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static void reset_props_strings(struct props *props)
|
||||||
|
{
|
||||||
|
spa_zero(props->clock_name);
|
||||||
|
spa_zero(props->clock_device);
|
||||||
|
spa_zero(props->clock_interface);
|
||||||
|
}
|
||||||
|
|
||||||
static void reset_props(struct props *props)
|
static void reset_props(struct props *props)
|
||||||
{
|
{
|
||||||
props->freewheel = DEFAULT_FREEWHEEL;
|
props->freewheel = DEFAULT_FREEWHEEL;
|
||||||
spa_zero(props->clock_name);
|
|
||||||
props->clock_id = CLOCK_MONOTONIC;
|
props->clock_id = CLOCK_MONOTONIC;
|
||||||
props->freewheel_wait = DEFAULT_FREEWHEEL_WAIT;
|
props->freewheel_wait = DEFAULT_FREEWHEEL_WAIT;
|
||||||
props->resync_ms = DEFAULT_RESYNC_MS;
|
props->resync_ms = DEFAULT_RESYNC_MS;
|
||||||
|
reset_props_strings(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const struct clock_info {
|
static const struct clock_info {
|
||||||
|
|
@ -598,10 +614,280 @@ static int impl_node_process(void *object)
|
||||||
return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA;
|
return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int impl_node_enum_params(void *object, int seq,
|
||||||
|
uint32_t id, uint32_t start, uint32_t num,
|
||||||
|
const struct spa_pod *filter)
|
||||||
|
{
|
||||||
|
struct impl *this = object;
|
||||||
|
struct spa_pod *param;
|
||||||
|
struct spa_pod_builder b = { 0 };
|
||||||
|
uint8_t buffer[4096];
|
||||||
|
struct spa_result_node_params result;
|
||||||
|
uint32_t count = 0;
|
||||||
|
|
||||||
|
spa_return_val_if_fail(this != NULL, -EINVAL);
|
||||||
|
spa_return_val_if_fail(num != 0, -EINVAL);
|
||||||
|
|
||||||
|
result.id = id;
|
||||||
|
result.next = start;
|
||||||
|
next:
|
||||||
|
result.index = result.next++;
|
||||||
|
|
||||||
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
||||||
|
|
||||||
|
switch (id) {
|
||||||
|
case SPA_PARAM_PropInfo:
|
||||||
|
{
|
||||||
|
struct props *p = &this->props;
|
||||||
|
|
||||||
|
switch (result.index) {
|
||||||
|
case 0:
|
||||||
|
param = spa_pod_builder_add_object(&b,
|
||||||
|
SPA_TYPE_OBJECT_PropInfo, id,
|
||||||
|
SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_clockId),
|
||||||
|
SPA_PROP_INFO_description, SPA_POD_String("The clock id (monotonic, realtime, etc.)"),
|
||||||
|
SPA_PROP_INFO_type, SPA_POD_String(clock_id_to_name(p->clock_id)));
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
param = spa_pod_builder_add_object(&b,
|
||||||
|
SPA_TYPE_OBJECT_PropInfo, id,
|
||||||
|
SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_clockDevice),
|
||||||
|
SPA_PROP_INFO_description, SPA_POD_String("The clock device (eg. /dev/ptp0)"),
|
||||||
|
SPA_PROP_INFO_type, SPA_POD_Stringn(p->clock_device, sizeof(p->clock_device)));
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
param = spa_pod_builder_add_object(&b,
|
||||||
|
SPA_TYPE_OBJECT_PropInfo, id,
|
||||||
|
SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_clockInterface),
|
||||||
|
SPA_PROP_INFO_description, SPA_POD_String("The clock network interface (eg. eth0)"),
|
||||||
|
SPA_PROP_INFO_type, SPA_POD_Stringn(p->clock_interface, sizeof(p->clock_interface)));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SPA_PARAM_Props:
|
||||||
|
{
|
||||||
|
struct props *p = &this->props;
|
||||||
|
|
||||||
|
switch (result.index) {
|
||||||
|
case 0:
|
||||||
|
param = spa_pod_builder_add_object(&b,
|
||||||
|
SPA_TYPE_OBJECT_Props, id,
|
||||||
|
SPA_PROP_clockId, SPA_POD_String(clock_id_to_name(p->clock_id))
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
param = spa_pod_builder_add_object(&b,
|
||||||
|
SPA_TYPE_OBJECT_Props, id,
|
||||||
|
SPA_PROP_clockDevice, SPA_POD_Stringn(p->clock_device, sizeof(p->clock_device))
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
param = spa_pod_builder_add_object(&b,
|
||||||
|
SPA_TYPE_OBJECT_Props, id,
|
||||||
|
SPA_PROP_clockInterface, SPA_POD_Stringn(p->clock_interface, sizeof(p->clock_interface))
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return -ENOENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spa_pod_filter(&b, &result.param, param, filter) < 0)
|
||||||
|
goto next;
|
||||||
|
|
||||||
|
spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
|
||||||
|
|
||||||
|
if (++count != num)
|
||||||
|
goto next;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int get_phc_index(struct spa_system *s, const char *name) {
|
||||||
|
#ifdef ETHTOOL_GET_TS_INFO
|
||||||
|
struct ethtool_ts_info info = {0};
|
||||||
|
struct ifreq ifr = {0};
|
||||||
|
int fd, err;
|
||||||
|
|
||||||
|
info.cmd = ETHTOOL_GET_TS_INFO;
|
||||||
|
strncpy(ifr.ifr_name, name, IFNAMSIZ - 1);
|
||||||
|
ifr.ifr_data = (char *) &info;
|
||||||
|
|
||||||
|
fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||||
|
if (fd < 0)
|
||||||
|
return -errno;
|
||||||
|
|
||||||
|
err = spa_system_ioctl(s, fd, SIOCETHTOOL, &ifr);
|
||||||
|
close(fd);
|
||||||
|
if (err < 0)
|
||||||
|
return -errno;
|
||||||
|
|
||||||
|
return info.phc_index;
|
||||||
|
#else
|
||||||
|
return -ENOTSUP;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool parse_clock_id(struct impl *this, const char *s)
|
||||||
|
{
|
||||||
|
int id = clock_name_to_id(s);
|
||||||
|
if (id == -1) {
|
||||||
|
spa_log_info(this->log, "unknown clock id '%s'", s);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this->props.clock_id = id;
|
||||||
|
if (this->clock_fd >= 0) {
|
||||||
|
close(this->clock_fd);
|
||||||
|
this->clock_fd = -1;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool parse_clock_device(struct impl *this, const char *s)
|
||||||
|
{
|
||||||
|
int fd = open(s, O_RDONLY);
|
||||||
|
if (fd == -1) {
|
||||||
|
spa_log_info(this->log, "failed to open clock device '%s': %m", s);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this->clock_fd >= 0) {
|
||||||
|
close(this->clock_fd);
|
||||||
|
}
|
||||||
|
this->clock_fd = fd;
|
||||||
|
this->props.clock_id = FD_TO_CLOCKID(this->clock_fd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool parse_clock_interface(struct impl *this, const char *s)
|
||||||
|
{
|
||||||
|
int phc_index = get_phc_index(this->data_system, s);
|
||||||
|
if (phc_index < 0) {
|
||||||
|
spa_log_info(this->log, "failed to get phc device index for interface '%s': %s",
|
||||||
|
s, spa_strerror(phc_index));
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
char dev[19];
|
||||||
|
spa_scnprintf(dev, sizeof(dev), "/dev/ptp%d", phc_index);
|
||||||
|
if (!parse_clock_device(this, dev)) {
|
||||||
|
spa_log_info(this->log, "failed to open clock device '%s' "
|
||||||
|
"for interface '%s': %m", dev, s);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ensure_clock_name(struct impl *this)
|
||||||
|
{
|
||||||
|
struct props *p = &this->props;
|
||||||
|
if (p->clock_name[0] == '\0') {
|
||||||
|
const char *name = clock_id_to_name(p->clock_id);
|
||||||
|
if (p->clock_device[0])
|
||||||
|
name = p->clock_device;
|
||||||
|
if (p->clock_interface[0])
|
||||||
|
name = p->clock_interface;
|
||||||
|
spa_scnprintf(p->clock_name, sizeof(p->clock_name),
|
||||||
|
"%s.%s", DEFAULT_CLOCK_PREFIX, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
|
||||||
|
const struct spa_pod *param)
|
||||||
|
{
|
||||||
|
struct impl *this = object;
|
||||||
|
|
||||||
|
spa_return_val_if_fail(this != NULL, -EINVAL);
|
||||||
|
|
||||||
|
switch (id) {
|
||||||
|
case SPA_PARAM_Props:
|
||||||
|
{
|
||||||
|
struct props *p = &this->props;
|
||||||
|
bool notify = false;
|
||||||
|
char buffer[CLOCK_NAME_MAX];
|
||||||
|
int count;
|
||||||
|
|
||||||
|
if (param == NULL) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Note that the length passed to SPA_POD_OPT_Stringn() also
|
||||||
|
* includes room for the null terminator, so the content of the
|
||||||
|
* buffer variable is always guaranteed to be null terminated. */
|
||||||
|
|
||||||
|
spa_zero(buffer);
|
||||||
|
count = spa_pod_parse_object(param,
|
||||||
|
SPA_TYPE_OBJECT_Props, NULL,
|
||||||
|
SPA_PROP_clockId, SPA_POD_OPT_Stringn(buffer, sizeof(buffer))
|
||||||
|
);
|
||||||
|
if (count && parse_clock_id(this, buffer))
|
||||||
|
{
|
||||||
|
reset_props_strings(p);
|
||||||
|
notify = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
spa_zero(buffer);
|
||||||
|
count = spa_pod_parse_object(param,
|
||||||
|
SPA_TYPE_OBJECT_Props, NULL,
|
||||||
|
SPA_PROP_clockDevice, SPA_POD_OPT_Stringn(buffer, sizeof(buffer))
|
||||||
|
);
|
||||||
|
if (count && parse_clock_device(this, buffer))
|
||||||
|
{
|
||||||
|
reset_props_strings(p);
|
||||||
|
strncpy(p->clock_device, buffer, sizeof(p->clock_device));
|
||||||
|
notify = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
spa_zero(buffer);
|
||||||
|
count = spa_pod_parse_object(param,
|
||||||
|
SPA_TYPE_OBJECT_Props, NULL,
|
||||||
|
SPA_PROP_clockInterface, SPA_POD_OPT_Stringn(buffer, sizeof(buffer))
|
||||||
|
);
|
||||||
|
if (count && parse_clock_interface(this, buffer))
|
||||||
|
{
|
||||||
|
reset_props_strings(p);
|
||||||
|
strncpy(p->clock_interface, buffer, sizeof(p->clock_interface));
|
||||||
|
notify = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notify)
|
||||||
|
{
|
||||||
|
ensure_clock_name(this);
|
||||||
|
spa_log_info(this->log, "%p: setting clock to '%s'", this, p->clock_name);
|
||||||
|
if (this->started) {
|
||||||
|
do_stop(this);
|
||||||
|
do_start(this);
|
||||||
|
}
|
||||||
|
emit_node_info(this, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return -ENOENT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static const struct spa_node_methods impl_node = {
|
static const struct spa_node_methods impl_node = {
|
||||||
SPA_VERSION_NODE_METHODS,
|
SPA_VERSION_NODE_METHODS,
|
||||||
.add_listener = impl_node_add_listener,
|
.add_listener = impl_node_add_listener,
|
||||||
.set_callbacks = impl_node_set_callbacks,
|
.set_callbacks = impl_node_set_callbacks,
|
||||||
|
.enum_params = impl_node_enum_params,
|
||||||
|
.set_param = impl_node_set_param,
|
||||||
.set_io = impl_node_set_io,
|
.set_io = impl_node_set_io,
|
||||||
.send_command = impl_node_send_command,
|
.send_command = impl_node_send_command,
|
||||||
.process = impl_node_process,
|
.process = impl_node_process,
|
||||||
|
|
@ -655,31 +941,6 @@ impl_get_size(const struct spa_handle_factory *factory,
|
||||||
return sizeof(struct impl);
|
return sizeof(struct impl);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int get_phc_index(struct spa_system *s, const char *name) {
|
|
||||||
#ifdef ETHTOOL_GET_TS_INFO
|
|
||||||
struct ethtool_ts_info info = {0};
|
|
||||||
struct ifreq ifr = {0};
|
|
||||||
int fd, err;
|
|
||||||
|
|
||||||
info.cmd = ETHTOOL_GET_TS_INFO;
|
|
||||||
strncpy(ifr.ifr_name, name, IFNAMSIZ - 1);
|
|
||||||
ifr.ifr_data = (char *) &info;
|
|
||||||
|
|
||||||
fd = socket(AF_INET, SOCK_DGRAM, 0);
|
|
||||||
if (fd < 0)
|
|
||||||
return -errno;
|
|
||||||
|
|
||||||
err = spa_system_ioctl(s, fd, SIOCETHTOOL, &ifr);
|
|
||||||
close(fd);
|
|
||||||
if (err < 0)
|
|
||||||
return -errno;
|
|
||||||
|
|
||||||
return info.phc_index;
|
|
||||||
#else
|
|
||||||
return -ENOTSUP;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
static int
|
||||||
impl_init(const struct spa_handle_factory *factory,
|
impl_init(const struct spa_handle_factory *factory,
|
||||||
struct spa_handle *handle,
|
struct spa_handle *handle,
|
||||||
|
|
@ -727,9 +988,10 @@ impl_init(const struct spa_handle_factory *factory,
|
||||||
this->info.max_input_ports = 0;
|
this->info.max_input_ports = 0;
|
||||||
this->info.max_output_ports = 0;
|
this->info.max_output_ports = 0;
|
||||||
this->info.flags = SPA_NODE_FLAG_RT;
|
this->info.flags = SPA_NODE_FLAG_RT;
|
||||||
this->params[0] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
|
this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
|
||||||
|
this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
|
||||||
this->info.params = this->params;
|
this->info.params = this->params;
|
||||||
this->info.n_params = 0;
|
this->info.n_params = N_NODE_PARAMS;
|
||||||
|
|
||||||
reset_props(&this->props);
|
reset_props(&this->props);
|
||||||
|
|
||||||
|
|
@ -742,37 +1004,17 @@ impl_init(const struct spa_handle_factory *factory,
|
||||||
spa_scnprintf(this->props.clock_name,
|
spa_scnprintf(this->props.clock_name,
|
||||||
sizeof(this->props.clock_name), "%s", s);
|
sizeof(this->props.clock_name), "%s", s);
|
||||||
} else if (spa_streq(k, "clock.id") && this->clock_fd < 0) {
|
} else if (spa_streq(k, "clock.id") && this->clock_fd < 0) {
|
||||||
this->props.clock_id = clock_name_to_id(s);
|
if (parse_clock_id(this, s))
|
||||||
if (this->props.clock_id == -1) {
|
reset_props_strings(&this->props);
|
||||||
spa_log_warn(this->log, "unknown clock id '%s'", s);
|
|
||||||
this->props.clock_id = DEFAULT_CLOCK_ID;
|
|
||||||
}
|
|
||||||
} else if (spa_streq(k, "clock.device")) {
|
} else if (spa_streq(k, "clock.device")) {
|
||||||
if (this->clock_fd >= 0) {
|
if (parse_clock_device(this, s)) {
|
||||||
close(this->clock_fd);
|
reset_props_strings(&this->props);
|
||||||
}
|
strncpy(this->props.clock_device, s, sizeof(this->props.clock_device)-1);
|
||||||
this->clock_fd = open(s, O_RDONLY);
|
|
||||||
|
|
||||||
if (this->clock_fd == -1) {
|
|
||||||
spa_log_warn(this->log, "failed to open clock device '%s': %m", s);
|
|
||||||
} else {
|
|
||||||
this->props.clock_id = FD_TO_CLOCKID(this->clock_fd);
|
|
||||||
}
|
}
|
||||||
} else if (spa_streq(k, "clock.interface") && this->clock_fd < 0) {
|
} else if (spa_streq(k, "clock.interface") && this->clock_fd < 0) {
|
||||||
int phc_index = get_phc_index(this->data_system, s);
|
if (parse_clock_interface(this, s)) {
|
||||||
if (phc_index < 0) {
|
reset_props_strings(&this->props);
|
||||||
spa_log_warn(this->log, "failed to get phc device index for interface '%s': %s",
|
strncpy(this->props.clock_interface, s, sizeof(this->props.clock_interface)-1);
|
||||||
s, spa_strerror(phc_index));
|
|
||||||
} else {
|
|
||||||
char dev[19];
|
|
||||||
spa_scnprintf(dev, sizeof(dev), "/dev/ptp%d", phc_index);
|
|
||||||
this->clock_fd = open(dev, O_RDONLY);
|
|
||||||
if (this->clock_fd == -1) {
|
|
||||||
spa_log_warn(this->log, "failed to open clock device '%s' "
|
|
||||||
"for interface '%s': %m", dev, s);
|
|
||||||
} else {
|
|
||||||
this->props.clock_id = FD_TO_CLOCKID(this->clock_fd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (spa_streq(k, "freewheel.wait")) {
|
} else if (spa_streq(k, "freewheel.wait")) {
|
||||||
this->props.freewheel_wait = atoi(s);
|
this->props.freewheel_wait = atoi(s);
|
||||||
|
|
@ -785,6 +1027,7 @@ impl_init(const struct spa_handle_factory *factory,
|
||||||
"%s.%s", DEFAULT_CLOCK_PREFIX,
|
"%s.%s", DEFAULT_CLOCK_PREFIX,
|
||||||
clock_id_to_name(this->props.clock_id));
|
clock_id_to_name(this->props.clock_id));
|
||||||
}
|
}
|
||||||
|
ensure_clock_name(this);
|
||||||
|
|
||||||
this->tracking = !clock_for_timerfd(this->props.clock_id);
|
this->tracking = !clock_for_timerfd(this->props.clock_id);
|
||||||
this->timer_clockid = this->tracking ? CLOCK_MONOTONIC : this->props.clock_id;
|
this->timer_clockid = this->tracking ? CLOCK_MONOTONIC : this->props.clock_id;
|
||||||
|
|
|
||||||
|
|
@ -900,6 +900,7 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case SPA_IO_Position:
|
case SPA_IO_Position:
|
||||||
this->io_position = data;
|
this->io_position = data;
|
||||||
|
this->recheck_format = true;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -144,21 +144,21 @@ custom_target('pipewire-uninstalled',
|
||||||
command: [ln, '-fs', meson.project_build_root() + '/@INPUT@', '@OUTPUT@'],
|
command: [ln, '-fs', meson.project_build_root() + '/@INPUT@', '@OUTPUT@'],
|
||||||
)
|
)
|
||||||
|
|
||||||
#desktop_file = i18n.merge_file(
|
desktop_file = i18n.merge_file(
|
||||||
# input : 'pipewire.desktop.in',
|
input : 'pipewire.desktop.in',
|
||||||
# output : 'pipewire.desktop',
|
output : 'pipewire.desktop',
|
||||||
# po_dir : po_dir,
|
po_dir : po_dir,
|
||||||
# type : 'desktop',
|
type : 'desktop',
|
||||||
# install : true,
|
install : true,
|
||||||
# install_dir : pipewire_sysconfdir / 'xdg' / 'autostart'
|
install_dir : pipewire_sysconfdir / 'xdg' / 'autostart'
|
||||||
#)
|
)
|
||||||
#
|
|
||||||
#desktop_utils = find_program('desktop-file-validate', required: false)
|
desktop_utils = find_program('desktop-file-validate', required: false)
|
||||||
#if desktop_utils.found()
|
if desktop_utils.found()
|
||||||
# test('Validate desktop file', desktop_utils,
|
test('Validate desktop file', desktop_utils,
|
||||||
# args: [ desktop_file ],
|
args: [ desktop_file ],
|
||||||
# )
|
)
|
||||||
#endif
|
endif
|
||||||
|
|
||||||
subdir('filter-chain')
|
subdir('filter-chain')
|
||||||
subdir('systemd')
|
subdir('systemd')
|
||||||
|
|
|
||||||
|
|
@ -229,8 +229,10 @@ struct impl {
|
||||||
struct spa_audio_aec *aec;
|
struct spa_audio_aec *aec;
|
||||||
uint32_t aec_blocksize;
|
uint32_t aec_blocksize;
|
||||||
|
|
||||||
unsigned int capture_ready:1;
|
struct spa_io_position *capture_position;
|
||||||
unsigned int sink_ready:1;
|
struct spa_io_position *sink_position;
|
||||||
|
uint32_t capture_cycle;
|
||||||
|
uint32_t sink_cycle;
|
||||||
|
|
||||||
unsigned int do_disconnect:1;
|
unsigned int do_disconnect:1;
|
||||||
|
|
||||||
|
|
@ -307,13 +309,24 @@ static void process(struct impl *impl)
|
||||||
const float *play_delayed[impl->play_info.channels];
|
const float *play_delayed[impl->play_info.channels];
|
||||||
float *out[impl->out_info.channels];
|
float *out[impl->out_info.channels];
|
||||||
struct spa_data *dd;
|
struct spa_data *dd;
|
||||||
uint32_t i, size;
|
uint32_t i;
|
||||||
uint32_t rindex, pindex, oindex, pdindex, avail;
|
uint32_t rindex, pindex, oindex, pdindex, size;
|
||||||
|
int32_t avail, pavail, pdavail;
|
||||||
|
|
||||||
size = impl->aec_blocksize;
|
size = impl->aec_blocksize;
|
||||||
|
|
||||||
/* First read a block from the playback and capture ring buffers */
|
/* First read a block from the capture ring buffer */
|
||||||
spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex);
|
avail = spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex);
|
||||||
|
while (avail >= (int32_t)size * 2) {
|
||||||
|
/* drop samples that are not needed this or next cycle. Note
|
||||||
|
* that samples are kept in the ringbuffer until next cycle if
|
||||||
|
* size is not equal to or divisible by quantum, to avoid
|
||||||
|
* discontinuity */
|
||||||
|
pw_log_debug("avail %d", avail);
|
||||||
|
spa_ringbuffer_read_update(&impl->rec_ring, rindex + size);
|
||||||
|
avail = spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex);
|
||||||
|
pw_log_debug("new avail %d, size %u", avail, size);
|
||||||
|
}
|
||||||
|
|
||||||
for (i = 0; i < impl->rec_info.channels; i++) {
|
for (i = 0; i < impl->rec_info.channels; i++) {
|
||||||
/* captured samples, with echo from sink */
|
/* captured samples, with echo from sink */
|
||||||
|
|
@ -331,19 +344,34 @@ static void process(struct impl *impl)
|
||||||
out[i] = &out_buf[i][0];
|
out[i] = &out_buf[i][0];
|
||||||
}
|
}
|
||||||
|
|
||||||
spa_ringbuffer_get_read_index(&impl->play_ring, &pindex);
|
pavail = spa_ringbuffer_get_read_index(&impl->play_ring, &pindex);
|
||||||
spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex);
|
pdavail = spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex);
|
||||||
|
|
||||||
if (impl->playback != NULL && (pout = pw_stream_dequeue_buffer(impl->playback)) == NULL) {
|
if (impl->playback != NULL && (pout = pw_stream_dequeue_buffer(impl->playback)) == NULL) {
|
||||||
pw_log_debug("out of playback buffers: %m");
|
pw_log_debug("out of playback buffers: %m");
|
||||||
|
|
||||||
/* playback stream may not yet be in streaming state, drop play
|
/* playback stream may not yet be in streaming state, drop play
|
||||||
* data to avoid introducing additional playback latency */
|
* data to avoid introducing additional playback latency */
|
||||||
spa_ringbuffer_read_update(&impl->play_ring, pindex + size);
|
spa_ringbuffer_read_update(&impl->play_ring, pindex + pavail);
|
||||||
spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + size);
|
spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + pdavail);
|
||||||
goto done;
|
goto done;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pavail > avail) {
|
||||||
|
/* drop too old samples from previous graph cycles */
|
||||||
|
pw_log_debug("pavail %d, dropping %d", pavail, pavail - avail);
|
||||||
|
spa_ringbuffer_read_update(&impl->play_ring, pindex + pavail - avail);
|
||||||
|
pavail = spa_ringbuffer_get_read_index(&impl->play_ring, &pindex);
|
||||||
|
pw_log_debug("new pavail %d, avail %d", pavail, avail);
|
||||||
|
}
|
||||||
|
if (pdavail > avail) {
|
||||||
|
/* drop too old samples from previous graph cycles */
|
||||||
|
pw_log_debug("pdavail %d, dropping %d", pdavail, pdavail - avail);
|
||||||
|
spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + pdavail - avail);
|
||||||
|
pdavail = spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex);
|
||||||
|
pw_log_debug("new pdavail %d, avail %d", pdavail, avail);
|
||||||
|
}
|
||||||
|
|
||||||
for (i = 0; i < impl->play_info.channels; i++) {
|
for (i = 0; i < impl->play_info.channels; i++) {
|
||||||
/* echo from sink */
|
/* echo from sink */
|
||||||
play[i] = &play_buf[i][0];
|
play[i] = &play_buf[i][0];
|
||||||
|
|
@ -431,7 +459,7 @@ static void process(struct impl *impl)
|
||||||
* available on the source */
|
* available on the source */
|
||||||
|
|
||||||
avail = spa_ringbuffer_get_read_index(&impl->out_ring, &oindex);
|
avail = spa_ringbuffer_get_read_index(&impl->out_ring, &oindex);
|
||||||
while (avail >= size) {
|
while (avail >= (int32_t)size) {
|
||||||
if ((cout = pw_stream_dequeue_buffer(impl->source)) != NULL) {
|
if ((cout = pw_stream_dequeue_buffer(impl->source)) != NULL) {
|
||||||
for (i = 0; i < impl->out_info.channels; i++) {
|
for (i = 0; i < impl->out_info.channels; i++) {
|
||||||
dd = &cout->buffer->datas[i];
|
dd = &cout->buffer->datas[i];
|
||||||
|
|
@ -454,8 +482,8 @@ static void process(struct impl *impl)
|
||||||
}
|
}
|
||||||
|
|
||||||
done:
|
done:
|
||||||
impl->sink_ready = false;
|
impl->capture_cycle = 0;
|
||||||
impl->capture_ready = false;
|
impl->sink_cycle = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void reset_buffers(struct impl *impl)
|
static void reset_buffers(struct impl *impl)
|
||||||
|
|
@ -479,8 +507,8 @@ static void reset_buffers(struct impl *impl)
|
||||||
spa_ringbuffer_get_read_index(&impl->play_ring, &index);
|
spa_ringbuffer_get_read_index(&impl->play_ring, &index);
|
||||||
spa_ringbuffer_read_update(&impl->play_ring, index + (sizeof(float) * (impl->buffer_delay)));
|
spa_ringbuffer_read_update(&impl->play_ring, index + (sizeof(float) * (impl->buffer_delay)));
|
||||||
|
|
||||||
impl->sink_ready = false;
|
impl->capture_cycle = 0;
|
||||||
impl->capture_ready = false;
|
impl->sink_cycle = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void capture_destroy(void *d)
|
static void capture_destroy(void *d)
|
||||||
|
|
@ -546,8 +574,11 @@ static void capture_process(void *data)
|
||||||
spa_ringbuffer_write_update(&impl->rec_ring, index + size);
|
spa_ringbuffer_write_update(&impl->rec_ring, index + size);
|
||||||
|
|
||||||
if (avail + size >= impl->aec_blocksize) {
|
if (avail + size >= impl->aec_blocksize) {
|
||||||
impl->capture_ready = true;
|
if (impl->capture_position)
|
||||||
if (impl->sink_ready)
|
impl->capture_cycle = impl->capture_position->clock.cycle;
|
||||||
|
else
|
||||||
|
pw_log_warn("no capture position");
|
||||||
|
if (impl->capture_cycle == impl->sink_cycle)
|
||||||
process(impl);
|
process(impl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -740,12 +771,26 @@ static void input_param_changed(void *data, uint32_t id, const struct spa_pod* p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void capture_io_changed(void *data, uint32_t id, void *area, uint32_t size)
|
||||||
|
{
|
||||||
|
struct impl *impl = data;
|
||||||
|
|
||||||
|
switch (id) {
|
||||||
|
case SPA_IO_Position:
|
||||||
|
impl->capture_position = area;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static const struct pw_stream_events capture_events = {
|
static const struct pw_stream_events capture_events = {
|
||||||
PW_VERSION_STREAM_EVENTS,
|
PW_VERSION_STREAM_EVENTS,
|
||||||
.destroy = capture_destroy,
|
.destroy = capture_destroy,
|
||||||
.state_changed = capture_state_changed,
|
.state_changed = capture_state_changed,
|
||||||
.process = capture_process,
|
.process = capture_process,
|
||||||
.param_changed = input_param_changed
|
.param_changed = input_param_changed,
|
||||||
|
.io_changed = capture_io_changed
|
||||||
};
|
};
|
||||||
|
|
||||||
static void source_destroy(void *d)
|
static void source_destroy(void *d)
|
||||||
|
|
@ -930,10 +975,15 @@ static void sink_process(void *data)
|
||||||
SPA_PTROFF(d->data, offs, void), size);
|
SPA_PTROFF(d->data, offs, void), size);
|
||||||
}
|
}
|
||||||
spa_ringbuffer_write_update(&impl->play_ring, index + size);
|
spa_ringbuffer_write_update(&impl->play_ring, index + size);
|
||||||
|
spa_ringbuffer_get_write_index(&impl->play_delayed_ring, &index);
|
||||||
|
spa_ringbuffer_write_update(&impl->play_delayed_ring, index + size);
|
||||||
|
|
||||||
if (avail + size >= impl->aec_blocksize) {
|
if (avail + size >= impl->aec_blocksize) {
|
||||||
impl->sink_ready = true;
|
if (impl->sink_position)
|
||||||
if (impl->capture_ready)
|
impl->sink_cycle = impl->sink_position->clock.cycle;
|
||||||
|
else
|
||||||
|
pw_log_warn("no sink position");
|
||||||
|
if (impl->capture_cycle == impl->sink_cycle)
|
||||||
process(impl);
|
process(impl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -955,12 +1005,27 @@ static const struct pw_stream_events playback_events = {
|
||||||
.state_changed = playback_state_changed,
|
.state_changed = playback_state_changed,
|
||||||
.param_changed = output_param_changed
|
.param_changed = output_param_changed
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static void sink_io_changed(void *data, uint32_t id, void *area, uint32_t size)
|
||||||
|
{
|
||||||
|
struct impl *impl = data;
|
||||||
|
|
||||||
|
switch (id) {
|
||||||
|
case SPA_IO_Position:
|
||||||
|
impl->sink_position = area;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static const struct pw_stream_events sink_events = {
|
static const struct pw_stream_events sink_events = {
|
||||||
PW_VERSION_STREAM_EVENTS,
|
PW_VERSION_STREAM_EVENTS,
|
||||||
.destroy = sink_destroy,
|
.destroy = sink_destroy,
|
||||||
.process = sink_process,
|
.process = sink_process,
|
||||||
.state_changed = sink_state_changed,
|
.state_changed = sink_state_changed,
|
||||||
.param_changed = output_param_changed
|
.param_changed = output_param_changed,
|
||||||
|
.io_changed = sink_io_changed
|
||||||
};
|
};
|
||||||
|
|
||||||
#define MAX_PARAMS 512u
|
#define MAX_PARAMS 512u
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,16 @@ struct node {
|
||||||
struct session *session;
|
struct session *session;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct igmp_recovery {
|
||||||
|
struct pw_timer timer;
|
||||||
|
int socket_fd;
|
||||||
|
struct sockaddr_storage mcast_addr;
|
||||||
|
socklen_t mcast_len;
|
||||||
|
uint32_t if_index;
|
||||||
|
bool is_ipv6;
|
||||||
|
uint32_t deadline;
|
||||||
|
};
|
||||||
|
|
||||||
struct impl {
|
struct impl {
|
||||||
struct pw_properties *props;
|
struct pw_properties *props;
|
||||||
|
|
||||||
|
|
@ -265,7 +275,11 @@ struct impl {
|
||||||
struct pw_registry *registry;
|
struct pw_registry *registry;
|
||||||
struct spa_hook registry_listener;
|
struct spa_hook registry_listener;
|
||||||
|
|
||||||
struct pw_timer timer;
|
struct pw_timer sap_send_timer;
|
||||||
|
|
||||||
|
/* This timer is used when the first start_sap() call fails because
|
||||||
|
* of an ENODEV error (see the start_sap() code for details) */
|
||||||
|
struct pw_timer start_sap_retry_timer;
|
||||||
|
|
||||||
char *ifname;
|
char *ifname;
|
||||||
uint32_t ttl;
|
uint32_t ttl;
|
||||||
|
|
@ -281,6 +295,10 @@ struct impl {
|
||||||
struct spa_source *sap_source;
|
struct spa_source *sap_source;
|
||||||
uint32_t cleanup_interval;
|
uint32_t cleanup_interval;
|
||||||
|
|
||||||
|
/* IGMP recovery (triggers when no SAP packets are
|
||||||
|
* received after the recovery deadline is reached) */
|
||||||
|
struct igmp_recovery igmp_recovery;
|
||||||
|
|
||||||
uint32_t max_sessions;
|
uint32_t max_sessions;
|
||||||
uint32_t n_sessions;
|
uint32_t n_sessions;
|
||||||
struct spa_list sessions;
|
struct spa_list sessions;
|
||||||
|
|
@ -288,7 +306,7 @@ struct impl {
|
||||||
char *extra_attrs_preamble;
|
char *extra_attrs_preamble;
|
||||||
char *extra_attrs_end;
|
char *extra_attrs_end;
|
||||||
|
|
||||||
char *ptp_mgmt_socket;
|
char *ptp_mgmt_socket_path;
|
||||||
int ptp_fd;
|
int ptp_fd;
|
||||||
uint32_t ptp_seq;
|
uint32_t ptp_seq;
|
||||||
uint8_t clock_id[8];
|
uint8_t clock_id[8];
|
||||||
|
|
@ -322,6 +340,7 @@ static const struct format_info *find_audio_format_info(const char *mime)
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int start_sap(struct impl *impl);
|
||||||
static int send_sap(struct impl *impl, struct session *sess, bool bye);
|
static int send_sap(struct impl *impl, struct session *sess, bool bye);
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -383,7 +402,7 @@ static bool is_multicast(struct sockaddr *sa, socklen_t salen)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int make_unix_socket(const char *path) {
|
static int make_unix_ptp_mgmt_socket(const char *path) {
|
||||||
struct sockaddr_un addr;
|
struct sockaddr_un addr;
|
||||||
|
|
||||||
spa_autoclose int fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
spa_autoclose int fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
||||||
|
|
@ -419,7 +438,7 @@ static int make_send_socket(
|
||||||
|
|
||||||
af = src->ss_family;
|
af = src->ss_family;
|
||||||
if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
|
if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
|
||||||
pw_log_error("socket failed: %m");
|
pw_log_error("socket() failed: %m");
|
||||||
return -errno;
|
return -errno;
|
||||||
}
|
}
|
||||||
if (bind(fd, (struct sockaddr*)src, src_len) < 0) {
|
if (bind(fd, (struct sockaddr*)src, src_len) < 0) {
|
||||||
|
|
@ -451,6 +470,9 @@ static int make_send_socket(
|
||||||
pw_log_warn("setsockopt(IPV6_MULTICAST_HOPS) failed: %m");
|
pw_log_warn("setsockopt(IPV6_MULTICAST_HOPS) failed: %m");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pw_log_info("sender socket up and running");
|
||||||
|
|
||||||
return fd;
|
return fd;
|
||||||
error:
|
error:
|
||||||
close(fd);
|
close(fd);
|
||||||
|
|
@ -458,7 +480,7 @@ error:
|
||||||
}
|
}
|
||||||
|
|
||||||
static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen,
|
static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen,
|
||||||
char *ifname)
|
char *ifname, struct igmp_recovery *igmp_recovery)
|
||||||
{
|
{
|
||||||
int af, fd, val, res;
|
int af, fd, val, res;
|
||||||
struct ifreq req;
|
struct ifreq req;
|
||||||
|
|
@ -468,13 +490,13 @@ static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen,
|
||||||
|
|
||||||
af = sa->ss_family;
|
af = sa->ss_family;
|
||||||
if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
|
if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) {
|
||||||
pw_log_error("socket failed: %m");
|
pw_log_error("socket() failed: %m");
|
||||||
return -errno;
|
return -errno;
|
||||||
}
|
}
|
||||||
val = 1;
|
val = 1;
|
||||||
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0) {
|
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0) {
|
||||||
res = -errno;
|
res = -errno;
|
||||||
pw_log_error("setsockopt failed: %m");
|
pw_log_error("setsockopt() failed: %m");
|
||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
spa_zero(req);
|
spa_zero(req);
|
||||||
|
|
@ -528,6 +550,16 @@ static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen,
|
||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Store multicast info for recovery */
|
||||||
|
igmp_recovery->socket_fd = fd;
|
||||||
|
igmp_recovery->mcast_addr = ba;
|
||||||
|
igmp_recovery->mcast_len = salen;
|
||||||
|
igmp_recovery->if_index = req.ifr_ifindex;
|
||||||
|
igmp_recovery->is_ipv6 = (af == AF_INET6);
|
||||||
|
pw_log_debug("stored %s multicast info: socket_fd=%d, "
|
||||||
|
"if_index=%d", igmp_recovery->is_ipv6 ?
|
||||||
|
"IPv6" : "IPv4", fd, req.ifr_ifindex);
|
||||||
|
|
||||||
if (bind(fd, (struct sockaddr*)&ba, salen) < 0) {
|
if (bind(fd, (struct sockaddr*)&ba, salen) < 0) {
|
||||||
res = -errno;
|
res = -errno;
|
||||||
pw_log_error("bind() failed: %m");
|
pw_log_error("bind() failed: %m");
|
||||||
|
|
@ -540,6 +572,9 @@ static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen,
|
||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pw_log_info("receiver socket up and running");
|
||||||
|
|
||||||
return fd;
|
return fd;
|
||||||
error:
|
error:
|
||||||
close(fd);
|
close(fd);
|
||||||
|
|
@ -548,8 +583,13 @@ error:
|
||||||
|
|
||||||
static bool update_ts_refclk(struct impl *impl)
|
static bool update_ts_refclk(struct impl *impl)
|
||||||
{
|
{
|
||||||
if (!impl->ptp_mgmt_socket || impl->ptp_fd < 0)
|
if (!impl->ptp_mgmt_socket_path)
|
||||||
return false;
|
return false;
|
||||||
|
if (impl->ptp_fd < 0) {
|
||||||
|
impl->ptp_fd = make_unix_ptp_mgmt_socket(impl->ptp_mgmt_socket_path);
|
||||||
|
if (impl->ptp_fd < 0)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Read if something is left in the socket
|
// Read if something is left in the socket
|
||||||
int avail;
|
int avail;
|
||||||
|
|
@ -581,6 +621,12 @@ static bool update_ts_refclk(struct impl *impl)
|
||||||
|
|
||||||
if (write(impl->ptp_fd, &req, sizeof(req)) == -1) {
|
if (write(impl->ptp_fd, &req, sizeof(req)) == -1) {
|
||||||
pw_log_warn("Failed to send PTP management request: %m");
|
pw_log_warn("Failed to send PTP management request: %m");
|
||||||
|
if (errno != ENOTCONN)
|
||||||
|
return false;
|
||||||
|
close(impl->ptp_fd);
|
||||||
|
impl->ptp_fd = make_unix_ptp_mgmt_socket(impl->ptp_mgmt_socket_path);
|
||||||
|
if (impl->ptp_fd > -1)
|
||||||
|
pw_log_info("Reopened PTP management socket");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -922,7 +968,98 @@ static int send_sap(struct impl *impl, struct session *sess, bool bye)
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void on_timer_event(void *data)
|
static void on_igmp_recovery_timer_event(void *data)
|
||||||
|
{
|
||||||
|
struct impl *impl = data;
|
||||||
|
char addr[128];
|
||||||
|
int res = 0;
|
||||||
|
|
||||||
|
/* Only attempt recovery if we have a valid socket and multicast address */
|
||||||
|
if (impl->igmp_recovery.socket_fd < 0) {
|
||||||
|
pw_log_debug("no socket, skipping IGMP recovery");
|
||||||
|
goto finish;
|
||||||
|
}
|
||||||
|
|
||||||
|
pw_net_get_ip(&impl->igmp_recovery.mcast_addr, addr, sizeof(addr), NULL, NULL);
|
||||||
|
pw_log_info("IGMP recovery triggered for %s", addr);
|
||||||
|
|
||||||
|
/* Force IGMP membership refresh by leaving the group first, then rejoin */
|
||||||
|
if (impl->igmp_recovery.is_ipv6) {
|
||||||
|
struct ipv6_mreq mr6;
|
||||||
|
memset(&mr6, 0, sizeof(mr6));
|
||||||
|
mr6.ipv6mr_multiaddr = ((struct sockaddr_in6*)&impl->igmp_recovery.mcast_addr)->sin6_addr;
|
||||||
|
mr6.ipv6mr_interface = impl->igmp_recovery.if_index;
|
||||||
|
|
||||||
|
/* Leave the group first */
|
||||||
|
res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IPV6, IPV6_LEAVE_GROUP,
|
||||||
|
&mr6, sizeof(mr6));
|
||||||
|
if (SPA_LIKELY(res == 0)) {
|
||||||
|
pw_log_info("left IPv6 multicast group");
|
||||||
|
} else {
|
||||||
|
if (errno == EADDRNOTAVAIL) {
|
||||||
|
pw_log_info("attempted to leave IPv6 multicast group, but "
|
||||||
|
"membership was already silently dropped");
|
||||||
|
} else {
|
||||||
|
pw_log_warn("failed to leave IPv6 multicast group: %m");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IPV6, IPV6_JOIN_GROUP,
|
||||||
|
&mr6, sizeof(mr6));
|
||||||
|
if (res < 0) {
|
||||||
|
pw_log_warn("failed to re-join IPv6 multicast group: %m");
|
||||||
|
} else {
|
||||||
|
pw_log_info("re-joined IPv6 multicast group successfully");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
struct ip_mreqn mr4;
|
||||||
|
memset(&mr4, 0, sizeof(mr4));
|
||||||
|
mr4.imr_multiaddr = ((struct sockaddr_in*)&impl->igmp_recovery.mcast_addr)->sin_addr;
|
||||||
|
mr4.imr_ifindex = impl->igmp_recovery.if_index;
|
||||||
|
|
||||||
|
/* Leave the group first */
|
||||||
|
res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IP, IP_DROP_MEMBERSHIP,
|
||||||
|
&mr4, sizeof(mr4));
|
||||||
|
if (SPA_LIKELY(res == 0)) {
|
||||||
|
pw_log_info("left IPv4 multicast group");
|
||||||
|
} else {
|
||||||
|
if (errno == EADDRNOTAVAIL) {
|
||||||
|
pw_log_info("attempted to leave IPv4 multicast group, but "
|
||||||
|
"membership was already silently dropped");
|
||||||
|
} else {
|
||||||
|
pw_log_warn("failed to leave IPv4 multicast group: %m");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
|
||||||
|
&mr4, sizeof(mr4));
|
||||||
|
if (res < 0) {
|
||||||
|
pw_log_warn("failed to re-join IPv4 multicast group: %m");
|
||||||
|
} else {
|
||||||
|
pw_log_info("re-joined IPv4 multicast group successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finish:
|
||||||
|
/* If rejoining failed, try again in 1 second. This can happen
|
||||||
|
* for example when the network interface went down, and is not
|
||||||
|
* yet up and running again, and ENODEV is returned as a result.
|
||||||
|
* Otherwise, continue with the usual deadline. */
|
||||||
|
pw_timer_queue_add(impl->timer_queue, &impl->igmp_recovery.timer,
|
||||||
|
&impl->igmp_recovery.timer.timeout,
|
||||||
|
((res == 0) ? impl->igmp_recovery.deadline : 1) * SPA_NSEC_PER_SEC,
|
||||||
|
on_igmp_recovery_timer_event, impl);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void rearm_igmp_recovery_timer(struct impl *impl)
|
||||||
|
{
|
||||||
|
pw_timer_queue_cancel(&impl->igmp_recovery.timer);
|
||||||
|
pw_timer_queue_add(impl->timer_queue, &impl->igmp_recovery.timer,
|
||||||
|
NULL, impl->igmp_recovery.deadline * SPA_NSEC_PER_SEC,
|
||||||
|
on_igmp_recovery_timer_event, impl);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_sap_send_timer_event(void *data)
|
||||||
{
|
{
|
||||||
struct impl *impl = data;
|
struct impl *impl = data;
|
||||||
struct session *sess, *tmp;
|
struct session *sess, *tmp;
|
||||||
|
|
@ -956,9 +1093,16 @@ static void on_timer_event(void *data)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pw_timer_queue_add(impl->timer_queue, &impl->timer,
|
pw_timer_queue_add(impl->timer_queue, &impl->sap_send_timer,
|
||||||
&impl->timer.timeout, SAP_INTERVAL_SEC * SPA_NSEC_PER_SEC,
|
&impl->sap_send_timer.timeout, SAP_INTERVAL_SEC * SPA_NSEC_PER_SEC,
|
||||||
on_timer_event, impl);
|
on_sap_send_timer_event, impl);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_start_sap_retry_timer_event(void *data)
|
||||||
|
{
|
||||||
|
struct impl *impl = data;
|
||||||
|
pw_log_debug("trying again to start SAP send after previous attempt failed with ENODEV");
|
||||||
|
start_sap(impl);
|
||||||
}
|
}
|
||||||
|
|
||||||
static struct session *session_find(struct impl *impl, const struct sdp_info *info)
|
static struct session *session_find(struct impl *impl, const struct sdp_info *info)
|
||||||
|
|
@ -1646,23 +1790,70 @@ on_sap_io(void *data, int fd, uint32_t mask)
|
||||||
buffer[len] = 0;
|
buffer[len] = 0;
|
||||||
if ((res = parse_sap(impl, buffer, len)) < 0)
|
if ((res = parse_sap(impl, buffer, len)) < 0)
|
||||||
pw_log_warn("error parsing SAP: %s", spa_strerror(res));
|
pw_log_warn("error parsing SAP: %s", spa_strerror(res));
|
||||||
|
|
||||||
|
rearm_igmp_recovery_timer(impl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static int start_sap(struct impl *impl)
|
static int start_sap(struct impl *impl)
|
||||||
{
|
{
|
||||||
int fd = -1, res;
|
int fd = -1, res = 0;
|
||||||
char addr[128] = "invalid";
|
char addr[128] = "invalid";
|
||||||
|
|
||||||
pw_log_info("starting SAP timer");
|
pw_log_info("starting SAP send timer");
|
||||||
if ((res = pw_timer_queue_add(impl->timer_queue, &impl->timer,
|
/* start_sap() might be called more than once. See the make_recv_socket()
|
||||||
|
* call below for why that can happen. In such a case, the timer was
|
||||||
|
* started already. The easiest way of handling it is to just cancel it.
|
||||||
|
* Such cases are not expected to occur often, so canceling and then
|
||||||
|
* adding the timer again is acceptable. */
|
||||||
|
pw_timer_queue_cancel(&impl->sap_send_timer);
|
||||||
|
if ((res = pw_timer_queue_add(impl->timer_queue, &impl->sap_send_timer,
|
||||||
NULL, SAP_INTERVAL_SEC * SPA_NSEC_PER_SEC,
|
NULL, SAP_INTERVAL_SEC * SPA_NSEC_PER_SEC,
|
||||||
on_timer_event, impl)) < 0) {
|
on_sap_send_timer_event, impl)) < 0) {
|
||||||
pw_log_error("can't add timer: %s", spa_strerror(res));
|
pw_log_error("can't add SAP send timer: %s", spa_strerror(res));
|
||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
if ((fd = make_recv_socket(&impl->sap_addr, impl->sap_len, impl->ifname)) < 0)
|
if ((fd = make_recv_socket(&impl->sap_addr, impl->sap_len, impl->ifname,
|
||||||
return fd;
|
&(impl->igmp_recovery))) < 0) {
|
||||||
|
/* If make_recv_socket() tries to create a socket and join to a multicast
|
||||||
|
* group while the network interfaces are not ready yet to do so
|
||||||
|
* (usually because a network manager component is still setting up
|
||||||
|
* those network interfaces), ENODEV will be returned. This is essentially
|
||||||
|
* a race condition. There is no discernible way to be notified when the
|
||||||
|
* network interfaces are ready for that operation, so the next best
|
||||||
|
* approach is to essentially do a form of polling by retrying the
|
||||||
|
* start_sap() call after some time. The start_sap_retry_timer exists
|
||||||
|
* precisely for that purpose. This means that ENODEV is not treated as
|
||||||
|
* an error, but instead, it triggers the creation of that timer. */
|
||||||
|
if (fd == -ENODEV) {
|
||||||
|
pw_log_warn("failed to create receiver socket because network device "
|
||||||
|
"is not ready and present yet; will try again");
|
||||||
|
|
||||||
|
pw_timer_queue_cancel(&impl->start_sap_retry_timer);
|
||||||
|
/* Use a 1-second retry interval. The network interfaces
|
||||||
|
* are likely to be up and running then. */
|
||||||
|
pw_timer_queue_add(impl->timer_queue, &impl->start_sap_retry_timer,
|
||||||
|
NULL, 1 * SPA_NSEC_PER_SEC,
|
||||||
|
on_start_sap_retry_timer_event, impl);
|
||||||
|
|
||||||
|
/* It is important to return 0 in this case. Otherwise, the nonzero return
|
||||||
|
* value will later be propagated through the core as an error. */
|
||||||
|
res = 0;
|
||||||
|
goto finish;
|
||||||
|
} else {
|
||||||
|
pw_log_error("failed to create socket: %s", spa_strerror(-fd));
|
||||||
|
/* If ENODEV was returned earlier, and the start_sap_retry_timer
|
||||||
|
* was consequently created, but then a non-ENODEV error occurred,
|
||||||
|
* the timer must be stopped and removed. */
|
||||||
|
pw_timer_queue_cancel(&impl->start_sap_retry_timer);
|
||||||
|
res = fd;
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cleanup the timer in case ENODEV occurred earlier, and this time,
|
||||||
|
* the socket creation succeeded. */
|
||||||
|
pw_timer_queue_cancel(&impl->start_sap_retry_timer);
|
||||||
|
|
||||||
pw_net_get_ip(&impl->sap_addr, addr, sizeof(addr), NULL, NULL);
|
pw_net_get_ip(&impl->sap_addr, addr, sizeof(addr), NULL, NULL);
|
||||||
pw_log_info("starting SAP listener on %s", addr);
|
pw_log_info("starting SAP listener on %s", addr);
|
||||||
|
|
@ -1673,11 +1864,15 @@ static int start_sap(struct impl *impl)
|
||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
rearm_igmp_recovery_timer(impl);
|
||||||
|
|
||||||
|
finish:
|
||||||
|
return res;
|
||||||
|
|
||||||
error:
|
error:
|
||||||
if (fd > 0)
|
if (fd > 0)
|
||||||
close(fd);
|
close(fd);
|
||||||
return res;
|
goto finish;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void node_event_info(void *data, const struct pw_node_info *info)
|
static void node_event_info(void *data, const struct pw_node_info *info)
|
||||||
|
|
@ -1807,7 +2002,9 @@ static void impl_destroy(struct impl *impl)
|
||||||
if (impl->core && impl->do_disconnect)
|
if (impl->core && impl->do_disconnect)
|
||||||
pw_core_disconnect(impl->core);
|
pw_core_disconnect(impl->core);
|
||||||
|
|
||||||
pw_timer_queue_cancel(&impl->timer);
|
pw_timer_queue_cancel(&impl->sap_send_timer);
|
||||||
|
pw_timer_queue_cancel(&impl->start_sap_retry_timer);
|
||||||
|
pw_timer_queue_cancel(&impl->igmp_recovery.timer);
|
||||||
if (impl->sap_source)
|
if (impl->sap_source)
|
||||||
pw_loop_destroy_source(impl->loop, impl->sap_source);
|
pw_loop_destroy_source(impl->loop, impl->sap_source);
|
||||||
|
|
||||||
|
|
@ -1821,7 +2018,7 @@ static void impl_destroy(struct impl *impl)
|
||||||
free(impl->extra_attrs_preamble);
|
free(impl->extra_attrs_preamble);
|
||||||
free(impl->extra_attrs_end);
|
free(impl->extra_attrs_end);
|
||||||
|
|
||||||
free(impl->ptp_mgmt_socket);
|
free(impl->ptp_mgmt_socket_path);
|
||||||
free(impl->ifname);
|
free(impl->ifname);
|
||||||
free(impl);
|
free(impl);
|
||||||
}
|
}
|
||||||
|
|
@ -1874,6 +2071,9 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
|
||||||
impl->ptp_fd = -1;
|
impl->ptp_fd = -1;
|
||||||
spa_list_init(&impl->sessions);
|
spa_list_init(&impl->sessions);
|
||||||
|
|
||||||
|
impl->igmp_recovery.socket_fd = -1;
|
||||||
|
impl->igmp_recovery.if_index = -1;
|
||||||
|
|
||||||
if (args == NULL)
|
if (args == NULL)
|
||||||
args = "";
|
args = "";
|
||||||
|
|
||||||
|
|
@ -1893,11 +2093,11 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
|
||||||
impl->ifname = str ? strdup(str) : NULL;
|
impl->ifname = str ? strdup(str) : NULL;
|
||||||
|
|
||||||
str = pw_properties_get(props, "ptp.management-socket");
|
str = pw_properties_get(props, "ptp.management-socket");
|
||||||
impl->ptp_mgmt_socket = str ? strdup(str) : NULL;
|
impl->ptp_mgmt_socket_path = str ? strdup(str) : NULL;
|
||||||
|
|
||||||
// TODO: support UDP management access as well
|
// TODO: support UDP management access as well
|
||||||
if (impl->ptp_mgmt_socket)
|
if (impl->ptp_mgmt_socket_path)
|
||||||
impl->ptp_fd = make_unix_socket(impl->ptp_mgmt_socket);
|
impl->ptp_fd = make_unix_ptp_mgmt_socket(impl->ptp_mgmt_socket_path);
|
||||||
|
|
||||||
if ((str = pw_properties_get(props, "sap.ip")) == NULL)
|
if ((str = pw_properties_get(props, "sap.ip")) == NULL)
|
||||||
str = DEFAULT_SAP_IP;
|
str = DEFAULT_SAP_IP;
|
||||||
|
|
@ -1909,6 +2109,11 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
|
||||||
impl->cleanup_interval = pw_properties_get_uint32(impl->props,
|
impl->cleanup_interval = pw_properties_get_uint32(impl->props,
|
||||||
"sap.cleanup.sec", DEFAULT_CLEANUP_SEC);
|
"sap.cleanup.sec", DEFAULT_CLEANUP_SEC);
|
||||||
|
|
||||||
|
/* We will use half of the cleanup interval for IGMP deadline, minimum 1 second */
|
||||||
|
impl->igmp_recovery.deadline = SPA_MAX(impl->cleanup_interval / 2, 1u);
|
||||||
|
pw_log_info("using IGMP deadline of %" PRIu32 " second(s)",
|
||||||
|
impl->igmp_recovery.deadline);
|
||||||
|
|
||||||
impl->ttl = pw_properties_get_uint32(props, "net.ttl", DEFAULT_TTL);
|
impl->ttl = pw_properties_get_uint32(props, "net.ttl", DEFAULT_TTL);
|
||||||
impl->mcast_loop = pw_properties_get_bool(props, "net.loop", DEFAULT_LOOP);
|
impl->mcast_loop = pw_properties_get_bool(props, "net.loop", DEFAULT_LOOP);
|
||||||
impl->max_sessions = pw_properties_get_uint32(props, "sap.max-sessions", DEFAULT_MAX_SESSIONS);
|
impl->max_sessions = pw_properties_get_uint32(props, "sap.max-sessions", DEFAULT_MAX_SESSIONS);
|
||||||
|
|
|
||||||
|
|
@ -1043,8 +1043,11 @@ on_data_io(void *data, int fd, uint32_t mask)
|
||||||
if (sess == NULL)
|
if (sess == NULL)
|
||||||
goto unknown_ssrc;
|
goto unknown_ssrc;
|
||||||
|
|
||||||
if (sess->data_ready && sess->receiving)
|
if (sess->data_ready && sess->receiving) {
|
||||||
rtp_stream_receive_packet(sess->recv, buffer, len);
|
uint64_t current_time = rtp_stream_get_nsec(sess->recv);
|
||||||
|
rtp_stream_receive_packet(sess->recv, buffer, len,
|
||||||
|
current_time);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
#include <net/if.h>
|
#include <net/if.h>
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
|
|
||||||
|
#include <spa/utils/atomic.h>
|
||||||
#include <spa/utils/hook.h>
|
#include <spa/utils/hook.h>
|
||||||
#include <spa/utils/result.h>
|
#include <spa/utils/result.h>
|
||||||
#include <spa/utils/ringbuffer.h>
|
#include <spa/utils/ringbuffer.h>
|
||||||
|
|
@ -156,6 +157,9 @@
|
||||||
PW_LOG_TOPIC(mod_topic, "mod." NAME);
|
PW_LOG_TOPIC(mod_topic, "mod." NAME);
|
||||||
#define PW_LOG_TOPIC_DEFAULT mod_topic
|
#define PW_LOG_TOPIC_DEFAULT mod_topic
|
||||||
|
|
||||||
|
#define DEFAULT_IGMP_CHECK_INTERVAL_SEC 5
|
||||||
|
#define DEFAULT_IGMP_DEADLINE_SEC 30
|
||||||
|
|
||||||
#define DEFAULT_CLEANUP_SEC 60
|
#define DEFAULT_CLEANUP_SEC 60
|
||||||
#define DEFAULT_SOURCE_IP "224.0.0.56"
|
#define DEFAULT_SOURCE_IP "224.0.0.56"
|
||||||
|
|
||||||
|
|
@ -180,6 +184,23 @@ static const struct spa_dict_item module_info[] = {
|
||||||
{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
|
{ PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct igmp_recovery {
|
||||||
|
struct pw_timer timer;
|
||||||
|
int socket_fd;
|
||||||
|
struct sockaddr_storage mcast_addr;
|
||||||
|
socklen_t mcast_len;
|
||||||
|
uint32_t if_index;
|
||||||
|
bool is_ipv6;
|
||||||
|
/* This is the interval the recovery timer runs at. The timer
|
||||||
|
* checks at each interval if recovery is required. This value
|
||||||
|
* is defined by the igmp.check.interval.sec property. */
|
||||||
|
uint32_t check_interval;
|
||||||
|
/* This is the deadline for packets to arrive. If the deadline
|
||||||
|
* is exceeded, an IGMP recovery is attempted. This value is
|
||||||
|
* defined by the igmp.deadline.sec property. */
|
||||||
|
uint32_t deadline;
|
||||||
|
};
|
||||||
|
|
||||||
struct impl {
|
struct impl {
|
||||||
struct pw_impl_module *module;
|
struct pw_impl_module *module;
|
||||||
struct spa_hook module_listener;
|
struct spa_hook module_listener;
|
||||||
|
|
@ -201,6 +222,15 @@ struct impl {
|
||||||
bool always_process;
|
bool always_process;
|
||||||
uint32_t cleanup_interval;
|
uint32_t cleanup_interval;
|
||||||
|
|
||||||
|
/* IGMP recovery (triggers when no RTP packets are
|
||||||
|
* received after the recovery deadline is reached) */
|
||||||
|
struct igmp_recovery igmp_recovery;
|
||||||
|
|
||||||
|
/* Monotonic timestamp of the last time a packet was
|
||||||
|
* received. This is accessed with atomic accessors
|
||||||
|
* to avoid race conditions. */
|
||||||
|
uint64_t last_packet_time;
|
||||||
|
|
||||||
struct pw_timer standby_timer;
|
struct pw_timer standby_timer;
|
||||||
/* This timer is used when the first stream_start() call fails because
|
/* This timer is used when the first stream_start() call fails because
|
||||||
* of an ENODEV error (see the stream_start() code for details) */
|
* of an ENODEV error (see the stream_start() code for details) */
|
||||||
|
|
@ -227,13 +257,6 @@ struct impl {
|
||||||
bool waiting;
|
bool waiting;
|
||||||
};
|
};
|
||||||
|
|
||||||
static inline uint64_t get_time_ns(void)
|
|
||||||
{
|
|
||||||
struct timespec ts;
|
|
||||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
||||||
return SPA_TIMESPEC_TO_NSEC(&ts);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int do_start(struct spa_loop *loop, bool async, uint32_t seq, const void *data,
|
static int do_start(struct spa_loop *loop, bool async, uint32_t seq, const void *data,
|
||||||
size_t size, void *user_data)
|
size_t size, void *user_data)
|
||||||
{
|
{
|
||||||
|
|
@ -261,6 +284,9 @@ on_rtp_io(void *data, int fd, uint32_t mask)
|
||||||
struct impl *impl = data;
|
struct impl *impl = data;
|
||||||
ssize_t len;
|
ssize_t len;
|
||||||
int suppressed;
|
int suppressed;
|
||||||
|
uint64_t current_time;
|
||||||
|
|
||||||
|
current_time = rtp_stream_get_nsec(impl->stream);
|
||||||
|
|
||||||
if (mask & SPA_IO_IN) {
|
if (mask & SPA_IO_IN) {
|
||||||
if ((len = recv(fd, impl->buffer, impl->buffer_size, 0)) < 0)
|
if ((len = recv(fd, impl->buffer, impl->buffer_size, 0)) < 0)
|
||||||
|
|
@ -270,10 +296,17 @@ on_rtp_io(void *data, int fd, uint32_t mask)
|
||||||
goto short_packet;
|
goto short_packet;
|
||||||
|
|
||||||
if (SPA_LIKELY(impl->stream)) {
|
if (SPA_LIKELY(impl->stream)) {
|
||||||
if (rtp_stream_receive_packet(impl->stream, impl->buffer, len) < 0)
|
if (rtp_stream_receive_packet(impl->stream, impl->buffer, len,
|
||||||
|
current_time) < 0)
|
||||||
goto receive_error;
|
goto receive_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Update last packet timestamp for IGMP recovery.
|
||||||
|
* The recovery timer will check this to see if recovery
|
||||||
|
* is necessary. Do this _before_ invoking do_start()
|
||||||
|
* in case the stream is waking up from standby. */
|
||||||
|
SPA_ATOMIC_STORE(impl->last_packet_time, current_time);
|
||||||
|
|
||||||
if (SPA_ATOMIC_LOAD(impl->state) != STATE_RECEIVING) {
|
if (SPA_ATOMIC_LOAD(impl->state) != STATE_RECEIVING) {
|
||||||
if (!SPA_ATOMIC_CAS(impl->state, STATE_PROBE, STATE_RECEIVING)) {
|
if (!SPA_ATOMIC_CAS(impl->state, STATE_PROBE, STATE_RECEIVING)) {
|
||||||
if (SPA_ATOMIC_CAS(impl->state, STATE_IDLE, STATE_RECEIVING))
|
if (SPA_ATOMIC_CAS(impl->state, STATE_IDLE, STATE_RECEIVING))
|
||||||
|
|
@ -284,17 +317,148 @@ on_rtp_io(void *data, int fd, uint32_t mask)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
receive_error:
|
receive_error:
|
||||||
if ((suppressed = spa_ratelimit_test(&impl->rate_limit, get_time_ns())) >= 0)
|
if ((suppressed = spa_ratelimit_test(&impl->rate_limit, current_time)) >= 0)
|
||||||
pw_log_warn("(%d suppressed) recv() error: %m", suppressed);
|
pw_log_warn("(%d suppressed) recv() error: %m", suppressed);
|
||||||
return;
|
return;
|
||||||
short_packet:
|
short_packet:
|
||||||
if ((suppressed = spa_ratelimit_test(&impl->rate_limit, get_time_ns())) >= 0)
|
if ((suppressed = spa_ratelimit_test(&impl->rate_limit, current_time)) >= 0)
|
||||||
pw_log_warn("(%d suppressed) short packet of len %zd received",
|
pw_log_warn("(%d suppressed) short packet of len %zd received",
|
||||||
suppressed, len);
|
suppressed, len);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int make_socket(const struct sockaddr* sa, socklen_t salen, char *ifname)
|
static int rejoin_igmp_group(struct spa_loop *loop, bool async, uint32_t seq,
|
||||||
|
const void *data, size_t size, void *user_data)
|
||||||
|
{
|
||||||
|
/* IMPORTANT: This must be run from within the data loop. */
|
||||||
|
|
||||||
|
int res;
|
||||||
|
struct impl *impl = user_data;
|
||||||
|
uint64_t current_time;
|
||||||
|
|
||||||
|
/* Force IGMP membership refresh by leaving the group first, then rejoin */
|
||||||
|
if (impl->igmp_recovery.is_ipv6) {
|
||||||
|
struct ipv6_mreq mr6;
|
||||||
|
memset(&mr6, 0, sizeof(mr6));
|
||||||
|
mr6.ipv6mr_multiaddr = ((struct sockaddr_in6*)&impl->igmp_recovery.mcast_addr)->sin6_addr;
|
||||||
|
mr6.ipv6mr_interface = impl->igmp_recovery.if_index;
|
||||||
|
|
||||||
|
/* Leave the group first */
|
||||||
|
res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IPV6, IPV6_LEAVE_GROUP,
|
||||||
|
&mr6, sizeof(mr6));
|
||||||
|
if (SPA_LIKELY(res == 0)) {
|
||||||
|
pw_log_info("left IPv6 multicast group");
|
||||||
|
} else {
|
||||||
|
if (errno == EADDRNOTAVAIL) {
|
||||||
|
pw_log_info("attempted to leave IPv6 multicast group, but "
|
||||||
|
"membership was already silently dropped");
|
||||||
|
} else {
|
||||||
|
pw_log_warn("failed to leave IPv6 multicast group: %m");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IPV6, IPV6_JOIN_GROUP,
|
||||||
|
&mr6, sizeof(mr6));
|
||||||
|
if (res < 0) {
|
||||||
|
pw_log_warn("failed to re-join IPv6 multicast group: %m");
|
||||||
|
} else {
|
||||||
|
pw_log_info("re-joined IPv6 multicast group successfully");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
struct ip_mreqn mr4;
|
||||||
|
memset(&mr4, 0, sizeof(mr4));
|
||||||
|
mr4.imr_multiaddr = ((struct sockaddr_in*)&impl->igmp_recovery.mcast_addr)->sin_addr;
|
||||||
|
mr4.imr_ifindex = impl->igmp_recovery.if_index;
|
||||||
|
|
||||||
|
/* Leave the group first */
|
||||||
|
res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IP, IP_DROP_MEMBERSHIP,
|
||||||
|
&mr4, sizeof(mr4));
|
||||||
|
if (SPA_LIKELY(res == 0)) {
|
||||||
|
pw_log_info("left IPv4 multicast group");
|
||||||
|
} else {
|
||||||
|
if (errno == EADDRNOTAVAIL) {
|
||||||
|
pw_log_info("attempted to leave IPv4 multicast group, but "
|
||||||
|
"membership was already silently dropped");
|
||||||
|
} else {
|
||||||
|
pw_log_warn("failed to leave IPv4 multicast group: %m");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
|
||||||
|
&mr4, sizeof(mr4));
|
||||||
|
if (res < 0) {
|
||||||
|
pw_log_warn("failed to re-join IPv4 multicast group: %m");
|
||||||
|
} else {
|
||||||
|
pw_log_info("re-joined IPv4 multicast group successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current_time = rtp_stream_get_nsec(impl->stream);
|
||||||
|
SPA_ATOMIC_STORE(impl->last_packet_time, current_time);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_igmp_recovery_timer_event(void *data)
|
||||||
|
{
|
||||||
|
int res;
|
||||||
|
struct impl *impl = data;
|
||||||
|
char addr[128];
|
||||||
|
uint64_t current_time, elapsed_seconds, last_packet_time;
|
||||||
|
|
||||||
|
/* Only attempt recovery if we have a valid socket and multicast address */
|
||||||
|
if (SPA_UNLIKELY(impl->igmp_recovery.socket_fd < 0)) {
|
||||||
|
pw_log_trace("no socket, skipping IGMP recovery");
|
||||||
|
goto finish;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This check if performed even if standby = false or
|
||||||
|
* receiving != STATE_RECEIVING , because the very reason
|
||||||
|
* for these states may be that the receiver socket was
|
||||||
|
* silently kicked out of the IGMP group (which causes data
|
||||||
|
* to no longer arrive, thus leading to these states). */
|
||||||
|
|
||||||
|
current_time = rtp_stream_get_nsec(impl->stream);
|
||||||
|
last_packet_time = SPA_ATOMIC_LOAD(impl->last_packet_time);
|
||||||
|
elapsed_seconds = (current_time - last_packet_time) / SPA_NSEC_PER_SEC;
|
||||||
|
|
||||||
|
/* Only trigger recovery if enough time has elapsed since last packet */
|
||||||
|
if (elapsed_seconds < impl->igmp_recovery.deadline) {
|
||||||
|
pw_log_trace("IGMP recovery check: %" PRIu64 " seconds elapsed, "
|
||||||
|
"need %" PRIu32 " seconds", elapsed_seconds,
|
||||||
|
impl->igmp_recovery.deadline);
|
||||||
|
goto finish;
|
||||||
|
}
|
||||||
|
|
||||||
|
pw_net_get_ip(&impl->igmp_recovery.mcast_addr, addr, sizeof(addr), NULL, NULL);
|
||||||
|
pw_log_info("starting IGMP recovery for %s", addr);
|
||||||
|
|
||||||
|
/* Run the actual recovery in the data loop, since recovery involves
|
||||||
|
* rejoining the socket to the IGMP group. By running this in the
|
||||||
|
* data loop, race conditions due to stray packets causing an on_rtp_io()
|
||||||
|
* invocation at the same time when the IGMP group rejoining takes place
|
||||||
|
* is avoided, since on_rtp_io() too runs in the data loop.
|
||||||
|
* This is a blocking call to make sure the rejoin attempt was fully
|
||||||
|
* done by the time this callback ends. (rejoin_igmp_group() does not
|
||||||
|
* do work that takes a long time to finish. )*/
|
||||||
|
res = pw_loop_locked(impl->data_loop, rejoin_igmp_group, 1, NULL, 0, impl);
|
||||||
|
|
||||||
|
if (SPA_LIKELY(res == 0)) {
|
||||||
|
pw_log_info("IGMP recovery for %s finished", addr);
|
||||||
|
} else {
|
||||||
|
pw_log_error("error while finishing IGMP recovery for %s: %s",
|
||||||
|
addr, spa_strerror(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
finish:
|
||||||
|
pw_timer_queue_add(impl->timer_queue, &impl->igmp_recovery.timer,
|
||||||
|
&impl->igmp_recovery.timer.timeout,
|
||||||
|
impl->igmp_recovery.check_interval * SPA_NSEC_PER_SEC,
|
||||||
|
on_igmp_recovery_timer_event, impl);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int make_socket(const struct sockaddr* sa, socklen_t salen, char *ifname,
|
||||||
|
struct igmp_recovery *igmp_recovery)
|
||||||
{
|
{
|
||||||
int af, fd, val, res;
|
int af, fd, val, res;
|
||||||
struct ifreq req;
|
struct ifreq req;
|
||||||
|
|
@ -374,6 +538,16 @@ static int make_socket(const struct sockaddr* sa, socklen_t salen, char *ifname)
|
||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Store multicast info for recovery */
|
||||||
|
igmp_recovery->socket_fd = fd;
|
||||||
|
igmp_recovery->mcast_addr = ba;
|
||||||
|
igmp_recovery->mcast_len = salen;
|
||||||
|
igmp_recovery->if_index = req.ifr_ifindex;
|
||||||
|
igmp_recovery->is_ipv6 = (af == AF_INET6);
|
||||||
|
pw_log_debug("stored %s multicast info: socket_fd=%d, "
|
||||||
|
"if_index=%d", igmp_recovery->is_ipv6 ?
|
||||||
|
"IPv6" : "IPv4", fd, req.ifr_ifindex);
|
||||||
|
|
||||||
if (bind(fd, (struct sockaddr*)&ba, salen) < 0) {
|
if (bind(fd, (struct sockaddr*)&ba, salen) < 0) {
|
||||||
res = -errno;
|
res = -errno;
|
||||||
pw_log_error("bind() failed: %m");
|
pw_log_error("bind() failed: %m");
|
||||||
|
|
@ -422,7 +596,8 @@ static void stream_open_connection(void *data, int *result)
|
||||||
pw_log_info("starting RTP listener");
|
pw_log_info("starting RTP listener");
|
||||||
|
|
||||||
if ((fd = make_socket((const struct sockaddr *)&impl->src_addr,
|
if ((fd = make_socket((const struct sockaddr *)&impl->src_addr,
|
||||||
impl->src_len, impl->ifname)) < 0) {
|
impl->src_len, impl->ifname,
|
||||||
|
&(impl->igmp_recovery))) < 0) {
|
||||||
/* If make_socket() tries to create a socket and join to a multicast
|
/* If make_socket() tries to create a socket and join to a multicast
|
||||||
* group while the network interfaces are not ready yet to do so
|
* group while the network interfaces are not ready yet to do so
|
||||||
* (usually because a network manager component is still setting up
|
* (usually because a network manager component is still setting up
|
||||||
|
|
@ -433,7 +608,7 @@ static void stream_open_connection(void *data, int *result)
|
||||||
* stream_start() call after some time. The stream_start_retry_timer exists
|
* stream_start() call after some time. The stream_start_retry_timer exists
|
||||||
* precisely for that purpose. This means that ENODEV is not treated as
|
* precisely for that purpose. This means that ENODEV is not treated as
|
||||||
* an error, but instead, it triggers the creation of that timer. */
|
* an error, but instead, it triggers the creation of that timer. */
|
||||||
if (errno == ENODEV) {
|
if (fd == -ENODEV) {
|
||||||
pw_log_warn("failed to create socket because network device is not ready "
|
pw_log_warn("failed to create socket because network device is not ready "
|
||||||
"and present yet; will try again");
|
"and present yet; will try again");
|
||||||
|
|
||||||
|
|
@ -449,12 +624,12 @@ static void stream_open_connection(void *data, int *result)
|
||||||
res = 0;
|
res = 0;
|
||||||
goto finish;
|
goto finish;
|
||||||
} else {
|
} else {
|
||||||
pw_log_error("failed to create socket: %m");
|
pw_log_error("failed to create socket: %s", spa_strerror(fd));
|
||||||
/* If ENODEV was returned earlier, and the stream_start_retry_timer
|
/* If ENODEV was returned earlier, and the stream_start_retry_timer
|
||||||
* was consequently created, but then a non-ENODEV error occurred,
|
* was consequently created, but then a non-ENODEV error occurred,
|
||||||
* the timer must be stopped and removed. */
|
* the timer must be stopped and removed. */
|
||||||
pw_timer_queue_cancel(&impl->stream_start_retry_timer);
|
pw_timer_queue_cancel(&impl->stream_start_retry_timer);
|
||||||
res = -errno;
|
res = fd;
|
||||||
goto finish;
|
goto finish;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -472,6 +647,13 @@ static void stream_open_connection(void *data, int *result)
|
||||||
goto finish;
|
goto finish;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((res = pw_timer_queue_add(impl->timer_queue, &impl->igmp_recovery.timer,
|
||||||
|
NULL, impl->igmp_recovery.check_interval * SPA_NSEC_PER_SEC,
|
||||||
|
on_igmp_recovery_timer_event, impl)) < 0) {
|
||||||
|
pw_log_error("can't add timer: %s", spa_strerror(res));
|
||||||
|
goto finish;
|
||||||
|
}
|
||||||
|
|
||||||
finish:
|
finish:
|
||||||
if (res != 0) {
|
if (res != 0) {
|
||||||
pw_log_error("failed to start RTP stream: %s", spa_strerror(res));
|
pw_log_error("failed to start RTP stream: %s", spa_strerror(res));
|
||||||
|
|
@ -495,6 +677,7 @@ static void stream_close_connection(void *data, int *result)
|
||||||
pw_log_info("stopping RTP listener");
|
pw_log_info("stopping RTP listener");
|
||||||
|
|
||||||
pw_timer_queue_cancel(&impl->stream_start_retry_timer);
|
pw_timer_queue_cancel(&impl->stream_start_retry_timer);
|
||||||
|
pw_timer_queue_cancel(&impl->igmp_recovery.timer);
|
||||||
|
|
||||||
pw_loop_destroy_source(impl->data_loop, impl->source);
|
pw_loop_destroy_source(impl->data_loop, impl->source);
|
||||||
impl->source = NULL;
|
impl->source = NULL;
|
||||||
|
|
@ -633,6 +816,7 @@ static void impl_destroy(struct impl *impl)
|
||||||
|
|
||||||
pw_timer_queue_cancel(&impl->standby_timer);
|
pw_timer_queue_cancel(&impl->standby_timer);
|
||||||
pw_timer_queue_cancel(&impl->stream_start_retry_timer);
|
pw_timer_queue_cancel(&impl->stream_start_retry_timer);
|
||||||
|
pw_timer_queue_cancel(&impl->igmp_recovery.timer);
|
||||||
|
|
||||||
if (impl->data_loop)
|
if (impl->data_loop)
|
||||||
pw_context_release_loop(impl->context, impl->data_loop);
|
pw_context_release_loop(impl->context, impl->data_loop);
|
||||||
|
|
@ -797,9 +981,20 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args)
|
||||||
* till we make it (or get timed out) */
|
* till we make it (or get timed out) */
|
||||||
pw_properties_set(stream_props, "rtp.receiving", "true");
|
pw_properties_set(stream_props, "rtp.receiving", "true");
|
||||||
|
|
||||||
impl->cleanup_interval = pw_properties_get_uint32(props,
|
impl->cleanup_interval = pw_properties_get_uint32(stream_props,
|
||||||
"cleanup.sec", DEFAULT_CLEANUP_SEC);
|
"cleanup.sec", DEFAULT_CLEANUP_SEC);
|
||||||
|
|
||||||
|
impl->igmp_recovery.check_interval = SPA_MAX(pw_properties_get_uint32(stream_props,
|
||||||
|
"igmp.check.interval.sec",
|
||||||
|
DEFAULT_IGMP_CHECK_INTERVAL_SEC), 1u);
|
||||||
|
pw_log_info("using IGMP check interval of %" PRIu32 " second(s)",
|
||||||
|
impl->igmp_recovery.check_interval);
|
||||||
|
|
||||||
|
impl->igmp_recovery.deadline = SPA_MAX(pw_properties_get_uint32(stream_props,
|
||||||
|
"igmp.deadline.sec", DEFAULT_IGMP_DEADLINE_SEC), 5u);
|
||||||
|
pw_log_info("using IGMP deadline of %" PRIu32 " second(s)",
|
||||||
|
impl->igmp_recovery.deadline);
|
||||||
|
|
||||||
impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core);
|
impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core);
|
||||||
if (impl->core == NULL) {
|
if (impl->core == NULL) {
|
||||||
str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
|
str = pw_properties_get(props, PW_KEY_REMOTE_NAME);
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,8 @@ static void rtp_audio_process_playback(void *data)
|
||||||
pw_stream_queue_buffer(impl->stream, buf);
|
pw_stream_queue_buffer(impl->stream, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int rtp_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len)
|
static int rtp_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len,
|
||||||
|
uint64_t current_time)
|
||||||
{
|
{
|
||||||
struct rtp_header *hdr;
|
struct rtp_header *hdr;
|
||||||
ssize_t hlen, plen;
|
ssize_t hlen, plen;
|
||||||
|
|
@ -273,7 +274,7 @@ static int rtp_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len)
|
||||||
timestamp = ntohl(hdr->timestamp) - impl->ts_offset;
|
timestamp = ntohl(hdr->timestamp) - impl->ts_offset;
|
||||||
|
|
||||||
impl->receiving = true;
|
impl->receiving = true;
|
||||||
impl->last_recv_timestamp = pw_stream_get_nsec(impl->stream);
|
impl->last_recv_timestamp = current_time;
|
||||||
|
|
||||||
plen = len - hlen;
|
plen = len - hlen;
|
||||||
samples = plen / stride;
|
samples = plen / stride;
|
||||||
|
|
|
||||||
|
|
@ -318,7 +318,8 @@ static int rtp_midi_receive_midi(struct impl *impl, uint8_t *packet, uint32_t ti
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int rtp_midi_receive(struct impl *impl, uint8_t *buffer, ssize_t len)
|
static int rtp_midi_receive(struct impl *impl, uint8_t *buffer, ssize_t len,
|
||||||
|
uint64_t current_time)
|
||||||
{
|
{
|
||||||
struct rtp_header *hdr;
|
struct rtp_header *hdr;
|
||||||
ssize_t hlen;
|
ssize_t hlen;
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,8 @@ static void rtp_opus_process_playback(void *data)
|
||||||
pw_stream_queue_buffer(impl->stream, buf);
|
pw_stream_queue_buffer(impl->stream, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int rtp_opus_receive(struct impl *impl, uint8_t *buffer, ssize_t len)
|
static int rtp_opus_receive(struct impl *impl, uint8_t *buffer, ssize_t len,
|
||||||
|
uint64_t current_time)
|
||||||
{
|
{
|
||||||
struct rtp_header *hdr;
|
struct rtp_header *hdr;
|
||||||
ssize_t hlen, plen;
|
ssize_t hlen, plen;
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,8 @@ struct impl {
|
||||||
* access below for the reason why. */
|
* access below for the reason why. */
|
||||||
uint8_t timer_running;
|
uint8_t timer_running;
|
||||||
|
|
||||||
int (*receive_rtp)(struct impl *impl, uint8_t *buffer, ssize_t len);
|
int (*receive_rtp)(struct impl *impl, uint8_t *buffer, ssize_t len,
|
||||||
|
uint64_t current_time);
|
||||||
/* Used for resetting the ring buffer before the stream starts, to prevent
|
/* Used for resetting the ring buffer before the stream starts, to prevent
|
||||||
* reading from uninitialized memory. This can otherwise happen in direct
|
* reading from uninitialized memory. This can otherwise happen in direct
|
||||||
* timestamp mode when the read index is set to an uninitialized location.
|
* timestamp mode when the read index is set to an uninitialized location.
|
||||||
|
|
@ -1036,10 +1037,17 @@ int rtp_stream_update_properties(struct rtp_stream *s, const struct spa_dict *di
|
||||||
return pw_stream_update_properties(impl->stream, dict);
|
return pw_stream_update_properties(impl->stream, dict);
|
||||||
}
|
}
|
||||||
|
|
||||||
int rtp_stream_receive_packet(struct rtp_stream *s, uint8_t *buffer, size_t len)
|
int rtp_stream_receive_packet(struct rtp_stream *s, uint8_t *buffer, size_t len,
|
||||||
|
uint64_t current_time)
|
||||||
{
|
{
|
||||||
struct impl *impl = (struct impl*)s;
|
struct impl *impl = (struct impl*)s;
|
||||||
return impl->receive_rtp(impl, buffer, len);
|
return impl->receive_rtp(impl, buffer, len, current_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t rtp_stream_get_nsec(struct rtp_stream *s)
|
||||||
|
{
|
||||||
|
struct impl *impl = (struct impl*)s;
|
||||||
|
return pw_stream_get_nsec(impl->stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t rtp_stream_get_time(struct rtp_stream *s, uint32_t *rate)
|
uint64_t rtp_stream_get_time(struct rtp_stream *s, uint32_t *rate)
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,10 @@ void rtp_stream_destroy(struct rtp_stream *s);
|
||||||
|
|
||||||
int rtp_stream_update_properties(struct rtp_stream *s, const struct spa_dict *dict);
|
int rtp_stream_update_properties(struct rtp_stream *s, const struct spa_dict *dict);
|
||||||
|
|
||||||
int rtp_stream_receive_packet(struct rtp_stream *s, uint8_t *buffer, size_t len);
|
int rtp_stream_receive_packet(struct rtp_stream *s, uint8_t *buffer, size_t len,
|
||||||
|
uint64_t current_time);
|
||||||
|
|
||||||
|
uint64_t rtp_stream_get_nsec(struct rtp_stream *s);
|
||||||
|
|
||||||
uint64_t rtp_stream_get_time(struct rtp_stream *s, uint32_t *rate);
|
uint64_t rtp_stream_get_time(struct rtp_stream *s, uint32_t *rate);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -609,7 +609,7 @@ static void test_array(const char *str, const char * const vals[])
|
||||||
|
|
||||||
spa_json_init(&it[0], str, strlen(str));
|
spa_json_init(&it[0], str, strlen(str));
|
||||||
if (spa_json_enter_array(&it[0], &it[1]) <= 0)
|
if (spa_json_enter_array(&it[0], &it[1]) <= 0)
|
||||||
spa_json_init(&it[1], str, strlen(str));
|
spa_json_init_relax(&it[1], '[', str, strlen(str));
|
||||||
for (i = 0; vals[i]; i++) {
|
for (i = 0; vals[i]; i++) {
|
||||||
pwtest_int_gt(spa_json_get_string(&it[1], val, sizeof(val)), 0);
|
pwtest_int_gt(spa_json_get_string(&it[1], val, sizeof(val)), 0);
|
||||||
pwtest_str_eq(val, vals[i]);
|
pwtest_str_eq(val, vals[i]);
|
||||||
|
|
@ -624,6 +624,7 @@ PWTEST(json_array)
|
||||||
test_array("[FL FR]", (const char *[]){ "FL", "FR", NULL });
|
test_array("[FL FR]", (const char *[]){ "FL", "FR", NULL });
|
||||||
test_array("FL FR", (const char *[]){ "FL", "FR", NULL });
|
test_array("FL FR", (const char *[]){ "FL", "FR", NULL });
|
||||||
test_array("[ FL FR ]", (const char *[]){ "FL", "FR", NULL });
|
test_array("[ FL FR ]", (const char *[]){ "FL", "FR", NULL });
|
||||||
|
test_array("FL FR FC", (const char *[]){ "FL", "FR", "FC", NULL });
|
||||||
|
|
||||||
return PWTEST_PASS;
|
return PWTEST_PASS;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue