Skip to content

fix(dlna): drive AVTransport v1, v2 and v3 renderers#23

Merged
stupside merged 1 commit into
stupside:mainfrom
Klaudioz:fix-avtransport-version
Jul 20, 2026
Merged

fix(dlna): drive AVTransport v1, v2 and v3 renderers#23
stupside merged 1 commit into
stupside:mainfrom
Klaudioz:fix-avtransport-version

Conversation

@Klaudioz

Copy link
Copy Markdown
Contributor

Problem

connectDLNA looked the transport service up by the exact URN urn:schemas-upnp-org:service:AVTransport:1, so renderers that publish a newer version could not be connected to — even though castor scan listed them:

INFO device found name=50PUD6654/43 type=dlna address=http://192.168.1.189:2870/dmr.xml
ERRO application error error="connecting to device: creating AVTransport client:
  goupnp: service \"urn:schemas-upnp-org:service:AVTransport:1\" not found within
  device \"50PUD6654/43\""

The gap between those two lines is version tolerance: SSDP M-SEARCH for MediaRenderer:1 is answered by a MediaRenderer:3 device, so discovery succeeds, and the exact-match failure only surfaces later when parsing the description.

The device in question, a Philips 50PUD6654/43, publishes:

deviceType   urn:schemas-upnp-org:device:MediaRenderer:3
serviceType  urn:schemas-upnp-org:service:RenderingControl:3
serviceType  urn:schemas-upnp-org:service:ConnectionManager:3
serviceType  urn:schemas-upnp-org:service:AVTransport:3

AVTransport is there — just not at v1. This should affect any renderer advertising v2 or v3.

Fix

Look the service up across v3, v2 and v1, newest first. UPnP requires a higher service version to remain backward compatible with the actions of lower ones, and castor calls only SetAVTransportURI and Play, both unchanged since v1.

dlnaDevice now holds the generic goupnp.ServiceClient instead of av1.AVTransport1. That wrapper hardcodes URN_AVTransport_1 as the SOAP action namespace in every PerformActionCtx call, so reusing it for a v3 service would send a mismatched SOAPACTION that a strict renderer can reject as an invalid action — and goupnp ships no av2/av3 package to switch to. The two actions are now issued against the service type the device actually published.

Tests

TestFindAVTransport covers v1-only (existing behaviour unchanged), v3-only, v2-only, newest-wins when several versions are published, a service nested in a sub-device, and a renderer exposing no AVTransport at all.

ok  github.com/stupside/castor/internal/device

Not verified

Service selection is tested against in-memory device descriptions. I have not yet confirmed end-to-end playback on the v3 renderer — go build ./... needs the third_party/whisper.cpp submodule, which wasn't checked out in my working copy, so I built and vetted ./internal/device/ only. Left as a draft for that reason; happy to confirm on hardware before it's marked ready.

https://claude.ai/code/session_01SapjS1H29hRzRbzJin212o

@stupside

stupside commented Jul 20, 2026

Copy link
Copy Markdown
Owner

@Klaudioz Thank you for the contrib ! Were you able to test it on your Philips 50PUD6654/43 ?

Note: I merged a branch requested by an other PR, there is some conflicts can you check it out ?

Service lookup matched only "urn:schemas-upnp-org:service:AVTransport:1",
an exact string compare, so renderers publishing a newer version failed to
connect even though SSDP discovery listed them: version-tolerant M-SEARCH
answers a MediaRenderer:1 probe from a MediaRenderer:3 device, and the
mismatch only surfaced later when parsing the description.

    creating AVTransport client: goupnp: service
    "urn:schemas-upnp-org:service:AVTransport:1" not found within device

Look services up across v3, v2 and v1, newest first, via a shared
findService helper. UPnP requires a higher service version to stay
backward compatible with the actions of lower ones, and castor calls only
SetAVTransportURI, Play and GetProtocolInfo, unchanged since v1.

Apply the same treatment to the ConnectionManager lookup added in stupside#22.
That one fails soft rather than hard: a renderer publishing only
ConnectionManager:3 degrades to fallbackCaps, so it re-encodes streams it
could have copied. The copy path was unreachable on exactly the renderers
this commit makes connectable.

Hold generic goupnp.ServiceClients rather than the av1 wrappers: those
hardcode the v1 URN as the SOAP action namespace, which a strict v3
service can reject as an invalid action.

Observed on a Philips 50PUD6654/43, which exposes RenderingControl:3,
ConnectionManager:3 and AVTransport:3 only.

Claude-Session: https://claude.ai/code/session_01SapjS1H29hRzRbzJin212o
@Klaudioz
Klaudioz force-pushed the fix-avtransport-version branch from 9b24f8c to 752654c Compare July 20, 2026 16:48
@Klaudioz

Copy link
Copy Markdown
Contributor Author

Thanks for taking a look!

Conflicts: rebased onto bc9fd8b — should be clean now.

Testing: confirmed working on the Philips 50PUD6654/43. It failed to connect at all before this change; it now connects and plays, and the log shows a negotiated renderer capabilities line, so the ConnectionManager path is exercised too rather than silently falling back.

While resolving the conflict I noticed negotiateCaps from #22 has the same version-locking issue, so I widened the fix. av1.NewConnectionManager1ClientsFromRootDevice is also pinned to :1, and this TV publishes ConnectionManager:3. That path fails soft rather than hard — it falls through to fallbackCaps() — so the symptom isn't a connection error but a silent loss of the passthrough path: the renderer gets re-encoded video it could have stream-copied. On this class of TV the copy path #22 added was unreachable.

Both lookups now go through a shared findService(root, loc, service) that tries v3 → v2 → v1.

One design note: I dropped the av1 wrappers in favour of the generic goupnp.ServiceClient. Those wrappers hardcode URN_AVTransport_1 as the SOAP action namespace in every PerformActionCtx call, so reusing them against a v3 service sends a SOAPACTION that doesn't match the service type — which a strict renderer can reject as an invalid action. goupnp ships no av2/av3 package, so actions are now issued against whatever version the device published. SetAVTransportURI, Play and GetProtocolInfo are unchanged since v1, so the request shapes are identical.

Unrelated, but hit it while building to test — make build fails on macOS at whisper.cpp 9386f23: the Metal .m files compile as C and that target's C_INCLUDES omits ggml/include, so ggml.h isn't found. Worked around with -DCMAKE_C_FLAGS="-I…/ggml/include". Happy to open a separate issue.

Happy to split the ConnectionManager change into its own PR if you'd rather keep them separate.

@Klaudioz
Klaudioz marked this pull request as ready for review July 20, 2026 19:26
@stupside stupside added enhancement New feature or request bug Something isn't working labels Jul 20, 2026
@stupside

stupside commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Thanks for taking a look!

Conflicts: rebased onto bc9fd8b — should be clean now.

Testing: confirmed working on the Philips 50PUD6654/43. It failed to connect at all before this change; it now connects and plays, and the log shows a negotiated renderer capabilities line, so the ConnectionManager path is exercised too rather than silently falling back.

While resolving the conflict I noticed negotiateCaps from #22 has the same version-locking issue, so I widened the fix. av1.NewConnectionManager1ClientsFromRootDevice is also pinned to :1, and this TV publishes ConnectionManager:3. That path fails soft rather than hard — it falls through to fallbackCaps() — so the symptom isn't a connection error but a silent loss of the passthrough path: the renderer gets re-encoded video it could have stream-copied. On this class of TV the copy path #22 added was unreachable.

Both lookups now go through a shared findService(root, loc, service) that tries v3 → v2 → v1.

One design note: I dropped the av1 wrappers in favour of the generic goupnp.ServiceClient. Those wrappers hardcode URN_AVTransport_1 as the SOAP action namespace in every PerformActionCtx call, so reusing them against a v3 service sends a SOAPACTION that doesn't match the service type — which a strict renderer can reject as an invalid action. goupnp ships no av2/av3 package, so actions are now issued against whatever version the device published. SetAVTransportURI, Play and GetProtocolInfo are unchanged since v1, so the request shapes are identical.

Unrelated, but hit it while building to test — make build fails on macOS at whisper.cpp 9386f23: the Metal .m files compile as C and that target's C_INCLUDES omits ggml/include, so ggml.h isn't found. Worked around with -DCMAKE_C_FLAGS="-I…/ggml/include". Happy to open a separate issue.

Happy to split the ConnectionManager change into its own PR if you'd rather keep them separate.

All good, one PR works fine, thank you for the fix I really appreciate it. For the make build I pushed the fix to main as I had the same problem after clearing my cache…

➜  castor git:(main) ✗ make build
cmake -S third_party/whisper.cpp -B third_party/whisper.cpp/build_go -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DWHISPER_BUILD_TESTS=OFF -DWHISPER_BUILD_EXAMPLES=OFF \
                -DCMAKE_C_FLAGS="-I/Users/kilian/Documents/git/castor/third_party/whisper.cpp/ggml/include"
CMake Deprecation Warning at CMakeLists.txt:1 (cmake_minimum_required):
  Compatibility with CMake < 3.10 will be removed from a future version of
  CMake.

  Update the VERSION argument <min> value.  Or, use the <min>...<max> syntax
  to tell CMake that the project requires at least <min> but has been updated
  to work with policies introduced by <max> or earlier.


-- The C compiler identification is AppleClang 21.0.0.21000101
-- The CXX compiler identification is AppleClang 21.0.0.21000101
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found Git: /usr/bin/git (found version "2.50.1 (Apple Git-155)")
-- The ASM compiler identification is AppleClang
-- Found assembler: /usr/bin/cc
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Success
-- Found Threads: TRUE
-- sccache found, compilation results will be cached. Disable with GGML_CCACHE=OFF.
-- CMAKE_SYSTEM_PROCESSOR: arm64
-- GGML_SYSTEM_ARCH: ARM
-- Including CPU backend
-- Accelerate framework found
-- Could NOT find OpenMP_C (missing: OpenMP_C_FLAGS OpenMP_C_LIB_NAMES) 
-- Could NOT find OpenMP_CXX (missing: OpenMP_CXX_FLAGS OpenMP_CXX_LIB_NAMES) 
-- Could NOT find OpenMP (missing: OpenMP_C_FOUND OpenMP_CXX_FOUND) 
CMake Warning at ggml/src/ggml-cpu/CMakeLists.txt:84 (message):
  OpenMP not found
Call Stack (most recent call first):
  ggml/src/CMakeLists.txt:445 (ggml_add_cpu_backend_variant_impl)


-- ARM detected
-- Performing Test GGML_COMPILER_SUPPORTS_FP16_FORMAT_I3E
-- Performing Test GGML_COMPILER_SUPPORTS_FP16_FORMAT_I3E - Failed
CMake Warning at ggml/src/ggml-cpu/CMakeLists.txt:146 (message):
  ARM -march/-mcpu not found, -mcpu=native will be used
Call Stack (most recent call first):
  ggml/src/CMakeLists.txt:445 (ggml_add_cpu_backend_variant_impl)


-- Performing Test GGML_MACHINE_SUPPORTS_dotprod
-- Performing Test GGML_MACHINE_SUPPORTS_dotprod - Success
-- Performing Test GGML_MACHINE_SUPPORTS_i8mm
-- Performing Test GGML_MACHINE_SUPPORTS_i8mm - Success
-- Performing Test GGML_MACHINE_SUPPORTS_sve
-- Performing Test GGML_MACHINE_SUPPORTS_sve - Failed
-- Performing Test GGML_MACHINE_SUPPORTS_nosve
-- Performing Test GGML_MACHINE_SUPPORTS_nosve - Success
-- Performing Test GGML_MACHINE_SUPPORTS_sme
-- Performing Test GGML_MACHINE_SUPPORTS_sme - Failed
-- Performing Test GGML_MACHINE_SUPPORTS_nosme
-- Performing Test GGML_MACHINE_SUPPORTS_nosme - Success
-- Checking for ARM features using flags:
--   -U__ARM_FEATURE_SVE
--   -U__ARM_FEATURE_SME
--   -mcpu=native+dotprod+i8mm+nosve+nosme
-- Performing Test HAVE_DOTPROD
-- Performing Test HAVE_DOTPROD - Success
-- Performing Test HAVE_SVE
-- Performing Test HAVE_SVE - Failed
-- Performing Test HAVE_MATMUL_INT8
-- Performing Test HAVE_MATMUL_INT8 - Success
-- Performing Test HAVE_FMA
-- Performing Test HAVE_FMA - Success
-- Performing Test HAVE_FP16_VECTOR_ARITHMETIC
-- Performing Test HAVE_FP16_VECTOR_ARITHMETIC - Success
-- Performing Test HAVE_SME
-- Performing Test HAVE_SME - Failed
-- Adding CPU backend variant ggml-cpu: -U__ARM_FEATURE_SVE;-U__ARM_FEATURE_SME;-mcpu=native+dotprod+i8mm+nosve+nosme 
-- Looking for dgemm_
-- Looking for dgemm_ - found
-- Found BLAS: /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Accelerate.framework
-- BLAS found, Libraries: /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Accelerate.framework
-- BLAS found, Includes: 
-- Including BLAS backend
-- Metal framework found
-- Including METAL backend
-- ggml version: 0.9.8
-- ggml commit:  9386f239
-- Configuring done (4.8s)
-- Generating done (0.0s)
-- Build files have been written to: /Users/kilian/Documents/git/castor/third_party/whisper.cpp/build_go
cmake --build third_party/whisper.cpp/build_go --target whisper -j
[  5%] Building CXX object ggml/src/CMakeFiles/ggml-base.dir/ggml-backend.cpp.o
[  5%] Building C object ggml/src/CMakeFiles/ggml-base.dir/ggml.c.o
[ 10%] Building CXX object ggml/src/CMakeFiles/ggml-base.dir/gguf.cpp.o
[ 10%] Building CXX object ggml/src/CMakeFiles/ggml-base.dir/ggml.cpp.o
[ 12%] Building CXX object ggml/src/CMakeFiles/ggml-base.dir/ggml-threading.cpp.o
[ 15%] Building C object ggml/src/CMakeFiles/ggml-base.dir/ggml-alloc.c.o
[ 20%] Building C object ggml/src/CMakeFiles/ggml-base.dir/ggml-quants.c.o
[ 20%] Building CXX object ggml/src/CMakeFiles/ggml-base.dir/ggml-opt.cpp.o
[ 22%] Linking CXX static library libggml-base.a
[ 22%] Built target ggml-base
[ 25%] Generate assembly for embedded Metal library
Embedding Metal library
[ 27%] Building C object ggml/src/CMakeFiles/ggml-cpu.dir/ggml-cpu/ggml-cpu.c.o
[ 30%] Building CXX object ggml/src/CMakeFiles/ggml-cpu.dir/ggml-cpu/hbm.cpp.o
[ 35%] Building CXX object ggml/src/CMakeFiles/ggml-cpu.dir/ggml-cpu/amx/amx.cpp.o
[ 35%] Building CXX object ggml/src/CMakeFiles/ggml-cpu.dir/ggml-cpu/ggml-cpu.cpp.o
[ 37%] Building CXX object ggml/src/CMakeFiles/ggml-cpu.dir/ggml-cpu/ops.cpp.o
[ 40%] Building CXX object ggml/src/CMakeFiles/ggml-cpu.dir/ggml-cpu/vec.cpp.o
[ 42%] Building C object ggml/src/CMakeFiles/ggml-cpu.dir/ggml-cpu/quants.c.o
[ 45%] Building CXX object ggml/src/CMakeFiles/ggml-cpu.dir/ggml-cpu/binary-ops.cpp.o
[ 47%] Building C object ggml/src/CMakeFiles/ggml-cpu.dir/ggml-cpu/arch/arm/quants.c.o
[ 50%] Building CXX object ggml/src/ggml-blas/CMakeFiles/ggml-blas.dir/ggml-blas.cpp.o
[ 52%] Building CXX object ggml/src/CMakeFiles/ggml-cpu.dir/ggml-cpu/traits.cpp.o
[ 55%] Building CXX object ggml/src/CMakeFiles/ggml-cpu.dir/ggml-cpu/repack.cpp.o
[ 57%] Building CXX object ggml/src/CMakeFiles/ggml-cpu.dir/ggml-cpu/arch/arm/repack.cpp.o
[ 60%] Building CXX object ggml/src/CMakeFiles/ggml-cpu.dir/ggml-cpu/amx/mmq.cpp.o
[ 62%] Building CXX object ggml/src/CMakeFiles/ggml-cpu.dir/ggml-cpu/unary-ops.cpp.o
[ 65%] Building C object ggml/src/ggml-metal/CMakeFiles/ggml-metal.dir/ggml-metal-device.m.o
[ 67%] Building CXX object ggml/src/ggml-metal/CMakeFiles/ggml-metal.dir/ggml-metal-device.cpp.o
[ 70%] Building CXX object ggml/src/ggml-metal/CMakeFiles/ggml-metal.dir/ggml-metal.cpp.o
[ 72%] Building ASM object ggml/src/ggml-metal/CMakeFiles/ggml-metal.dir/autogenerated/ggml-metal-embed.s.o
[ 75%] Building CXX object ggml/src/ggml-metal/CMakeFiles/ggml-metal.dir/ggml-metal-ops.cpp.o
[ 77%] Building CXX object ggml/src/ggml-metal/CMakeFiles/ggml-metal.dir/ggml-metal-common.cpp.o
[ 80%] Building C object ggml/src/ggml-metal/CMakeFiles/ggml-metal.dir/ggml-metal-context.m.o
[ 82%] Linking CXX static library libggml-blas.a
[ 85%] Linking CXX static library libggml-cpu.a
clang: warning: argument unused during compilation: '-D GGML_METAL_EMBED_LIBRARY' [-Wunused-command-line-argument]
clang: warning: argument unused during compilation: '-D GGML_SCHED_MAX_COPIES=4' [-Wunused-command-line-argument]
clang: warning: argument unused during compilation: '-D _DARWIN_C_SOURCE' [-Wunused-command-line-argument]
clang: warning: argument unused during compilation: '-D _XOPEN_SOURCE=600' [-Wunused-command-line-argument]
clang: warning: argument unused during compilation: '-O3' [-Wunused-command-line-argument]
clang: warning: argument unused during compilation: '-D NDEBUG' [-Wunused-command-line-argument]
[ 85%] Built target ggml-blas
[ 87%] Linking CXX static library libggml-metal.a
[ 87%] Built target ggml-cpu
[ 87%] Built target ggml-metal
[ 92%] Building CXX object ggml/src/CMakeFiles/ggml.dir/ggml-backend-dl.cpp.o
[ 92%] Building CXX object ggml/src/CMakeFiles/ggml.dir/ggml-backend-reg.cpp.o
[ 95%] Linking CXX static library libggml.a
[ 95%] Built target ggml
[ 97%] Building CXX object src/CMakeFiles/whisper.dir/whisper.cpp.o
[100%] Linking CXX static library libwhisper.a
[100%] Built target whisper
go build -o castor .

@stupside
stupside merged commit b18f045 into stupside:main Jul 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants